| | |
| | | this.isInitializing = true; |
| | | this.taxonomiesToFetch = new Set(); |
| | | |
| | | this.store = new window.jvbStore({ |
| | | name: `taxonomies`, |
| | | storeName: `terms`, |
| | | keyPath: 'id', |
| | | showLoading: false, |
| | | indexes: [ |
| | | {name: 'taxonomy', keyPath: 'taxonomy'}, |
| | | {name: 'parent', keyPath: 'parent'}, |
| | | {name: 'slug', keyPath: 'slug', unique: true}, |
| | | {name: 'count', keyPath: 'count'}, |
| | | ], |
| | | endpoint: 'terms', |
| | | TTL: 7200000, //2 hours |
| | | filters: { |
| | | taxonomy: '', |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }, |
| | | required: 'taxonomy' |
| | | }); |
| | | this.triggers = new Set(['.taxonomy-toggle']); |
| | | |
| | | this.subscribers = new Set(); |
| | | |
| | | const store = window.jvbStore.register( |
| | | 'taxonomies', |
| | | { |
| | | storeName: `terms`, |
| | | keyPath: 'id', |
| | | showLoading: false, |
| | | indexes: [ |
| | | {name: 'taxonomy', keyPath: 'taxonomy'}, |
| | | {name: 'parent', keyPath: 'parent'}, |
| | | {name: 'slug', keyPath: 'slug', unique: true}, |
| | | {name: 'count', keyPath: 'count'}, |
| | | ], |
| | | endpoint: 'terms', |
| | | TTL: 2 * 60 * 1000, //2 hours |
| | | filters: { |
| | | taxonomy: '', |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }, |
| | | required: 'taxonomy', |
| | | delayFetch: true, |
| | | }); |
| | | this.store = store.terms; |
| | | |
| | | // Central field management |
| | | this.fields = new Map(); |
| | |
| | | this.currentConfig = null; |
| | | this.currentSingular = null; |
| | | this.currentPlural = null; |
| | | this.activeStore = null; |
| | | |
| | | // Modal state |
| | | this.disabled = false; |
| | |
| | | this.scanExistingFields(); |
| | | this.initGlobalListeners(); |
| | | |
| | | if (this.hasAutocomplete && window.jvbTaxCreator) { |
| | | this.creator = new window.jvbTaxCreator(this); |
| | | } |
| | | this.store.subscribe(this.handleStoreEvent.bind(this)); |
| | | // Complete initialization |
| | | this.isInitializing = false; |
| | |
| | | handleStoreEvent(event, data) { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | // Only render if modal is open OR if it's an autocomplete request |
| | | const taxonomy = this.store.filters.taxonomy; |
| | | // Handle batch taxonomy loading (comma-separated) |
| | | if (taxonomy?.includes(',')) { |
| | | this.handleBatchDataLoaded(taxonomy, data); |
| | | } |
| | | // Update button states for this taxonomy (or taxonomies) |
| | | if (taxonomy) { |
| | | // Handle comma-separated taxonomies from batch fetch |
| | | const taxonomies = taxonomy.includes(',') |
| | | ? taxonomy.split(',').map(t => t.trim()) |
| | | : [taxonomy]; |
| | | |
| | | taxonomies.forEach(tax => { |
| | | this.updateFieldsForTaxonomy(tax); |
| | | }); |
| | | } |
| | | |
| | | // Only render if modal is open OR autocomplete active |
| | | if (this.modal?.open) { |
| | | this.handleTermsLoaded(data); |
| | | } |
| | | // Handle autocomplete results |
| | | |
| | | if (this.isAutocompleteActive && this.activeField) { |
| | | const field = this.fields.get(this.activeField); |
| | | const terms = data.data?.items || []; |
| | |
| | | break; |
| | | |
| | | case 'filters-changed': |
| | | // Modal UI updates happen here if needed |
| | | if (this.modal?.open) { |
| | | this.showLoading(); |
| | | } |
| | |
| | | */ |
| | | handleTermsLoaded(data) { |
| | | this.hideLoading(); |
| | | const terms = data.data?.items || []; |
| | | const pagination = data.data?.pagination || {}; |
| | | const terms = this.store.getFiltered(); // Use getFiltered() instead of getFilteredItems() |
| | | const response = this.store.lastResponse?.page || {}; |
| | | const isSearch = data.filters?.search && data.filters.search.length > 0; |
| | | const append = data.filters?.page > 1; |
| | | const append = response.page > 1; |
| | | |
| | | this.notify('terms-loaded', { terms, filters: data.filters }); |
| | | |
| | | if (terms.length === 0) { |
| | | if (!append) { |
| | |
| | | this.observer.unobserve(this.ui.sentinel); |
| | | } else { |
| | | this.renderTerms(terms, append, isSearch); |
| | | this.currentTerms = terms; |
| | | |
| | | // Handle pagination |
| | | if (pagination.has_more) { |
| | | if (response.has_more) { |
| | | this.observer.observe(this.ui.sentinel); |
| | | } else { |
| | | this.observer.unobserve(this.ui.sentinel); |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Check if taxonomy has terms and update button states |
| | | */ |
| | | updateFieldButtonState(fieldId) { |
| | | const field = this.fields.get(fieldId); |
| | | if (!field) return; |
| | | |
| | | // Check store for items of this specific taxonomy |
| | | const hasTerms = Array.from(this.store.data.values()) |
| | | .some(term => term.taxonomy === field.taxonomy); |
| | | |
| | | if (field.toggle) { |
| | | field.toggle.disabled = !hasTerms && !field.canCreate; |
| | | field.toggle.title = !hasTerms |
| | | ? `No ${this.getSingular(field.taxonomy)} available` |
| | | : `Select ${this.getPlural(field.taxonomy)}`; |
| | | } |
| | | } |
| | | /** |
| | | * Update fields when taxonomy items are updated |
| | | */ |
| | | updateFieldsForTaxonomy(taxonomy, items) { |
| | | this.fields.forEach(field => { |
| | | if (field.taxonomy === taxonomy && field.selectedTerms.size > 0) { |
| | | // Update display with fresh term data |
| | | field.selectedTerms.forEach(termId => { |
| | | const term = items.find(item => item.id === termId); |
| | | if (term) { |
| | | const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`); |
| | | if (selectedItem) { |
| | | selectedItem.dataset.path = term.path; |
| | | selectedItem.querySelector('span').textContent = term.path; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | updateFieldsForTaxonomy(taxonomy) { |
| | | this.getFieldsForTaxonomy(taxonomy).forEach(field => { |
| | | this.updateFieldButtonState(field.id); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Get fields for a specific taxonomy |
| | | */ |
| | | getFieldsForTaxonomy(taxonomy) { |
| | | return Array.from(this.fields.values()) |
| | | .filter(field => field.taxonomy === taxonomy); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Scan page for existing taxonomy fields and register them |
| | | */ |
| | | scanExistingFields(container = null) { |
| | |
| | | registerField(field, options = {}) { |
| | | let input = field.querySelector('input[type=hidden]'); |
| | | if (!input) { |
| | | return; |
| | | return false; |
| | | } |
| | | |
| | | if (!('fieldId' in field.dataset)) { |
| | | field.dataset.fieldId = this.createFieldId(field); |
| | | } |
| | | let fieldId = field.dataset.fieldId; |
| | | let button = field.querySelector('button.taxonomy-toggle'); |
| | | |
| | | let button = (Object.hasOwn(options, 'button')) ? options.button : field.querySelector('button.taxonomy-toggle'); |
| | | |
| | | if (Object.hasOwn(options, 'buttonSelector')) { |
| | | this.triggers.add(options.buttonSelector); |
| | | } |
| | | |
| | | let config = { |
| | | id: fieldId, |
| | |
| | | isRequired: 'required' in button.dataset, |
| | | selectedTerms: new Set(), |
| | | toggle: button, |
| | | selectedContainer: field.querySelector('.selected-items'), |
| | | selectedContainer: (Object.hasOwn(options, 'selected')) ? options.selected : field.querySelector('.selected-items'), |
| | | ...options |
| | | }; |
| | | |
| | |
| | | selectedIds.forEach(id => config.selectedTerms.add(id)); |
| | | } |
| | | |
| | | if (Object.hasOwn(options, 'selectedItems')) { |
| | | options.selectedItems.forEach(id => { |
| | | config.selectedTerms.add(id); |
| | | }); |
| | | } |
| | | |
| | | this.fields.set(fieldId, config); |
| | | |
| | | // Ensure store exists for this taxonomy |
| | | if (this.isInitializing) { |
| | | this.taxonomiesToFetch.add(config.taxonomy); |
| | | } else { |
| | | this.store.setFilter('taxonomy', config.taxonomy); |
| | | // this.store.setFilter('taxonomy', config.taxonomy); |
| | | } |
| | | |
| | | // Initialize display for any pre-selected values |
| | |
| | | } |
| | | |
| | | /** |
| | | * Batch fetch all unique taxonomies collected during init |
| | | * Register a filter button (simplified registration for feed blocks) |
| | | */ |
| | | async batchFetchTaxonomies() { |
| | | if (this.taxonomiesToFetch.size === 0) return; |
| | | registerFilterButton(button, options = {}) { |
| | | const fieldId = this.createFieldId(button); |
| | | button.dataset.fieldId = fieldId; |
| | | |
| | | const taxonomies = Array.from(this.taxonomiesToFetch); |
| | | this.taxonomiesToFetch.clear(); |
| | | |
| | | console.log(`Batch fetching ${taxonomies.length} unique taxonomies:`, taxonomies); |
| | | |
| | | // Fetch each taxonomy sequentially (cache will prevent duplicates) |
| | | for (const taxonomy of taxonomies) { |
| | | await this.store.setFilters({ |
| | | taxonomy: taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }); |
| | | if (options.buttonSelector) { |
| | | this.triggers.add(options.buttonSelector); |
| | | } |
| | | |
| | | // Now initialize field displays |
| | | this.fields.forEach((config, fieldId) => { |
| | | if (config.selectedTerms.size > 0) { |
| | | this.initFieldDisplay(fieldId); |
| | | } |
| | | }); |
| | | const config = { |
| | | id: fieldId, |
| | | input: null, |
| | | container: options.container || button.closest('.filters') || button.parentElement, |
| | | taxonomy: button.dataset.taxonomy, |
| | | name: `filter_${button.dataset.taxonomy}`, |
| | | maxSelection: parseInt(button.dataset.max) || 0, |
| | | canSearch: 'search' in button.dataset, |
| | | hasAutocomplete: false, |
| | | canCreate: false, |
| | | isRequired: false, |
| | | selectedTerms: new Set(options.selectedItems || []), |
| | | toggle: button, |
| | | selectedContainer: options.selected || null, |
| | | isFilterMode: true, |
| | | ...options |
| | | }; |
| | | |
| | | this.fields.set(fieldId, config); |
| | | |
| | | if (this.isInitializing) { |
| | | this.taxonomiesToFetch.add(config.taxonomy); |
| | | } else { |
| | | this.store.setFilter('taxonomy', config.taxonomy); |
| | | } |
| | | |
| | | return fieldId; |
| | | } |
| | | |
| | | /** |
| | |
| | | if (!field || field.selectedTerms.size === 0) return; |
| | | |
| | | const selectedIds = Array.from(field.selectedTerms); |
| | | const cachedTerms = []; |
| | | |
| | | selectedIds.forEach(termId => { |
| | | const term = this.store.data.get(termId); |
| | | const term = this.store.get(termId); // Changed from getItem |
| | | if (term) { |
| | | cachedTerms.push(term); |
| | | this.addTermToDisplay(fieldId, term.id, term.name, term.path); |
| | | } |
| | | }); |
| | | |
| | | // Display all found terms |
| | | cachedTerms.forEach(term => { |
| | | this.addTermToDisplay(fieldId, term.id, term.name, term.path); |
| | | }); |
| | | |
| | | // Don't fetch missing terms here - they should be loaded by batchFetchTaxonomies |
| | | } |
| | | |
| | | /** |
| | |
| | | this.modalInstance.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'modal-open': |
| | | console.log(data); |
| | | this.openModal(data); |
| | | break; |
| | | case 'modal-close': |
| | |
| | | // Initialize intersection observer for infinite scroll |
| | | this.observer = new IntersectionObserver((entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting && this.activeStore) { |
| | | if (entry.isIntersecting) { |
| | | this.loadMoreTerms(); |
| | | } |
| | | }); |
| | |
| | | |
| | | initAutocomplete() |
| | | { |
| | | console.log('Autocomplete init'); |
| | | this.autocompleteHandler = window.debounce((e) => this.handleAutocomplete(e), 300); |
| | | this.autocompleteHandler = (e) => { |
| | | window.debouncer.schedule( |
| | | 'taxonomy-autocomplete', |
| | | () => this.handleAutocomplete(e), |
| | | 300 |
| | | ); |
| | | }; |
| | | document.addEventListener('input', this.autocompleteHandler); |
| | | document.addEventListener('blur', this.cleanupAutocomplete.bind(this)); |
| | | // Preload taxonomy data on focus |
| | | document.addEventListener('focus', (e) => { |
| | | if (!('autocomplete' in e.target.dataset)) { |
| | | return; |
| | | } |
| | | |
| | | const fieldId = this.getFieldId(e.target); |
| | | const field = this.fields.get(fieldId); |
| | | |
| | | if (!field) return; |
| | | |
| | | // Preload this taxonomy's data |
| | | this.preloadTaxonomy(field.taxonomy); |
| | | }, true); // Use capture phase |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | handleClick(e) { |
| | | // Handle taxonomy toggle buttons |
| | | const toggleButton = window.targetCheck(e, '.taxonomy-toggle'); |
| | | const toggleButton = window.targetCheck(e, Array.from(this.triggers)); |
| | | |
| | | if (toggleButton) { |
| | | e.preventDefault(); |
| | | this.handleToggleClick(toggleButton); |
| | |
| | | return; |
| | | } |
| | | |
| | | this.setActiveField(fieldId); |
| | | this.modalInstance.handleOpen(); |
| | | |
| | | this.setActiveField(fieldId, true); |
| | | |
| | | } catch (error) { |
| | | console.error('Error handling toggle click:', error); |
| | | this.error?.handleError(error, { |
| | | component: 'TaxonomySelector', |
| | | action: 'handleToggleClick' |
| | | }); |
| | | if (this.error?.log) { |
| | | this.error.log(error, { |
| | | component: 'TaxonomySelector', |
| | | action: 'handleToggleClick' |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Set the active field for modal operations |
| | | */ |
| | | setActiveField(fieldId) { |
| | | setActiveField(fieldId, openModal = false) { |
| | | this.activeField = fieldId; |
| | | this.currentConfig = this.fields.get(fieldId); |
| | | |
| | | this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single; |
| | | this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural; |
| | | this.currentSingular = this.getSingular(this.currentConfig.taxonomy); |
| | | this.currentPlural = this.getPlural(this.currentConfig.taxonomy); |
| | | |
| | | // Get or create store for this taxonomy |
| | | if (openModal) { |
| | | this.modalInstance.handleOpen(); |
| | | } |
| | | |
| | | // Set taxonomy filter - store handles the rest |
| | | this.store.setFilter('taxonomy', this.currentConfig.taxonomy); |
| | | |
| | | // Clear modal selection state |
| | | this.selectedTerms.clear(); |
| | | |
| | | // Copy field's current selections to modal state |
| | | if (this.currentConfig.selectedTerms) { |
| | | let termsToFetch = []; |
| | | this.currentConfig.selectedTerms.forEach(termId => { |
| | | const term = this.store.getItem(termId); |
| | | if (term) { |
| | | this.selectedTerms.set(termId, { |
| | | id: termId, |
| | | name: term.name, |
| | | path: term.path |
| | | }); |
| | | } else { |
| | | termsToFetch.push(termId); |
| | | } |
| | | }); |
| | | if (termsToFetch.length > 0) { |
| | | let terms = this.fetchSpecificTerms(termsToFetch); |
| | | terms.forEach(term => { |
| | | this.selectedTerms.set(term.id, { |
| | | id: term.id, |
| | | name: term.name, |
| | | path: term.path |
| | | }); |
| | | this.currentConfig.selectedTerms.forEach(termId => { |
| | | const term = this.store.get(termId); |
| | | if (term) { |
| | | this.selectedTerms.set(termId, { |
| | | id: termId, |
| | | name: term.name, |
| | | path: term.path |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | fetchSpecificTerms(terms) { |
| | | return []; |
| | | } |
| | | |
| | | /** |
| | | * Handle clicks within modal |
| | |
| | | filterCallback: callback // Store the callback |
| | | }); |
| | | |
| | | this.setActiveField(virtualFieldId); |
| | | this.setActiveField(virtualFieldId, true); |
| | | this.modalInstance.handleOpen(); |
| | | } |
| | | |
| | | /** |
| | | * Open modal and initialize |
| | | */ |
| | | openModal(config) { |
| | | this.activeField = config.fieldId; |
| | | this.currentConfig = config; |
| | | openModal() { |
| | | if (!this.currentConfig) { |
| | | console.error('No active field set'); |
| | | return; |
| | | } |
| | | |
| | | // Initialize creator if available |
| | | if (config.canCreate && 'jvbTaxCreator' in window) { |
| | | if (!this.creator && this.currentConfig.canCreate && 'jvbTaxCreator' in window) { |
| | | this.creator = new window.jvbTaxCreator(this); |
| | | } else if (this.creator) { |
| | | delete this.creator; |
| | | } |
| | | |
| | | // Load selected terms into modal state |
| | | this.selectedTerms = new Set(config.selectedTerms); |
| | | // Update modal UI |
| | | this.updateModalForTaxonomy(); |
| | | |
| | | // Only fetch if taxonomy changed |
| | | const currentTaxonomy = this.store.filters.taxonomy; |
| | | if (currentTaxonomy !== config.taxonomy) { |
| | | this.store.setFilters({ |
| | | taxonomy: config.taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }); |
| | | } |
| | | |
| | | // Reset UI |
| | | window.removeChildren(this.ui.termsList); |
| | | this.ui.search.value = ''; |
| | | // Load selected terms display |
| | | this.updateModalSelections(); |
| | | this.updateSelectionCount(); |
| | | |
| | | this.modalInstance.open(); |
| | | // Clear terms list and show loading |
| | | window.removeChildren(this.ui.termsList); |
| | | this.showLoading(); |
| | | } |
| | | |
| | | /** |
| | | * Update selection count display in modal |
| | | */ |
| | | updateSelectionCount() { |
| | | if (!this.currentConfig) return; |
| | | |
| | | const count = this.selectedTerms.size; |
| | | const max = this.currentConfig.maxSelection; |
| | | |
| | | // Update any count display elements |
| | | const countElement = this.modal?.querySelector('.selection-count'); |
| | | if (countElement) { |
| | | if (max > 0) { |
| | | countElement.textContent = `${count} of ${max} selected`; |
| | | } else { |
| | | countElement.textContent = `${count} selected`; |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Get singular label for taxonomy |
| | | */ |
| | | getSingular(taxonomy) { |
| | | return jvbSettings.labels[taxonomy]?.single || taxonomy; |
| | | } |
| | | |
| | | /** |
| | | * Get plural label for taxonomy |
| | | */ |
| | | getPlural(taxonomy) { |
| | | return jvbSettings.labels[taxonomy]?.plural || taxonomy; |
| | | } |
| | | |
| | | /** |
| | |
| | | this.observer.unobserve(this.ui.sentinel); |
| | | window.removeChildren(this.ui.termsList); |
| | | |
| | | this.notify('selected-terms', { |
| | | terms: this.selectedTerms, |
| | | taxonomy: this.currentConfig.taxonomy |
| | | }); |
| | | |
| | | if (this.currentConfig?.isFilterMode) { |
| | | if (this.currentConfig.filterCallback) { |
| | | const selectedIds = Array.from(this.selectedTerms.keys()); |
| | | this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy); |
| | | } |
| | | this.fields.delete(this.activeField); |
| | | // this.fields.delete(this.activeField); |
| | | } else if (this.activeField) { |
| | | this.saveSelectionsToField(this.activeField); |
| | | } |
| | |
| | | this.ui.search.input.removeEventListener('input', this.searchHandler); |
| | | } |
| | | |
| | | if (this.creator) { |
| | | if (!this.hasAutocomplete && this.creator) { |
| | | delete this.creator; |
| | | } |
| | | |
| | |
| | | * Navigate to parent term |
| | | */ |
| | | navigateToParent() { |
| | | // Single call instead of two setFilter + manual fetch |
| | | // Store handles fetch automatically |
| | | this.store.setFilters({ |
| | | parent: 0, |
| | | page: 1 |
| | |
| | | * Navigate to child term |
| | | */ |
| | | navigateToChild(termId, termName) { |
| | | // Single call - auto-fetches |
| | | // Store handles fetch automatically |
| | | this.store.setFilters({ |
| | | parent: termId, |
| | | page: 1 |
| | |
| | | navigateToPath(pathLevel) { |
| | | const parentId = parseInt(pathLevel.dataset.id) || 0; |
| | | |
| | | // Single call - auto-fetches |
| | | // Store handles fetch automatically |
| | | this.store.setFilters({ |
| | | parent: parentId, |
| | | page: 1 |
| | |
| | | * Load more terms (pagination) |
| | | */ |
| | | loadMoreTerms() { |
| | | if (!this.activeStore) return; |
| | | |
| | | const currentPage = this.activeStore.filters.page || 1; |
| | | const currentPage = this.store.filters.page || 1; |
| | | this.store.setFilter('page', currentPage + 1); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Render terms list |
| | | */ |
| | | renderTerms(terms, append = false, showPath = false) { |
| | | renderTerms(terms = null, append = false, showPath = false) { |
| | | // If no terms provided, get from store |
| | | if (!terms) { |
| | | terms = this.store.getFiltered(); |
| | | } |
| | | |
| | | if (!append) { |
| | | window.removeChildren(this.ui.termsList); |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | // Use this.store instead of this.activeStore |
| | | const currentParent = this.store.filters.parent || 0; |
| | | this.ui.breadcrumbs.back.hidden = currentParent === 0; |
| | | |
| | | const fragment = document.createDocumentFragment(); |
| | | terms.forEach(term => { |
| | | const element = this.createTermElement({ |
| | | id: parseInt(term.id), |
| | |
| | | }); |
| | | |
| | | if (element) { |
| | | this.ui.termsList.appendChild(element); |
| | | fragment.appendChild(element); |
| | | } |
| | | }); |
| | | |
| | | this.ui.termsList.appendChild(fragment); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | return null; |
| | | } |
| | | /******************************************** |
| | | BATCH FETCH: fetches first page for all taxonomies in one call |
| | | ********************************************/ |
| | | async batchFetchTaxonomies() { |
| | | if (this.taxonomiesToFetch.size === 0) return; |
| | | |
| | | const taxonomies = Array.from(this.taxonomiesToFetch); |
| | | this.taxonomiesToFetch.clear(); |
| | | |
| | | // Single fetch - the data-loaded event will handle cache splitting |
| | | this.store.setFilters({ |
| | | taxonomy: taxonomies.join(','), |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }); |
| | | } |
| | | handleBatchDataLoaded(taxonomyString, data) { |
| | | const taxonomies = taxonomyString.split(',').map(t => t.trim()); |
| | | const storeInstance = this.store.getStore(); // Access actual store instance |
| | | |
| | | taxonomies.forEach(taxonomy => { |
| | | const filters = { |
| | | taxonomy: taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }; |
| | | |
| | | // Use the internal generateCacheKey method via store instance |
| | | const cacheKey = this.generateCacheKeyForFilters(filters); |
| | | |
| | | // Filter items for this specific taxonomy |
| | | const items = Array.from(this.store.data.values()) |
| | | .filter(item => item.taxonomy === taxonomy) |
| | | .map(item => item.id); |
| | | |
| | | const cacheEntry = { |
| | | key: cacheKey, |
| | | items: items, |
| | | timestamp: Date.now(), |
| | | endpoint: storeInstance.config.endpoint, |
| | | filters: filters |
| | | }; |
| | | |
| | | // Set in both memory and IndexedDB cache |
| | | storeInstance.cache.set(cacheKey, cacheEntry); |
| | | |
| | | // Persist to IndexedDB (if available) |
| | | if (storeInstance.db?.objectStoreNames.contains('cache')) { |
| | | const tx = storeInstance.db.transaction(['cache'], 'readwrite'); |
| | | const objectStore = tx.objectStore('cache'); |
| | | objectStore.put(cacheEntry); |
| | | } |
| | | |
| | | // Update button states for this taxonomy |
| | | this.updateFieldsForTaxonomy(taxonomy); |
| | | }); |
| | | |
| | | // Initialize field displays |
| | | this.fields.forEach((config, fieldId) => { |
| | | if (config.selectedTerms.size > 0) { |
| | | this.initFieldDisplay(fieldId); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Generate cache key for given filters (matching DataStore's internal logic) |
| | | */ |
| | | generateCacheKeyForFilters(filters) { |
| | | const normalized = Object.keys(filters) |
| | | .sort() |
| | | .reduce((acc, key) => { |
| | | acc[key] = filters[key]; |
| | | return acc; |
| | | }, {}); |
| | | |
| | | return JSON.stringify(normalized); |
| | | } |
| | | |
| | | /** |
| | | * Preload taxonomy data on hover |
| | | */ |
| | | async preloadTaxonomy(taxonomy) { |
| | | // Trigger fetch for this taxonomy |
| | | this.store.setFilters({ |
| | | taxonomy: taxonomy, |
| | | page: 1, |
| | | search: '', |
| | | parent: 0 |
| | | }); |
| | | } |
| | | /***************************************** |
| | | SUBSCRIBERS |
| | | *****************************************/ |
| | | |
| | | subscribe(callback) { |
| | | this.subscribers.add(callback); |
| | | return () => this.subscribers.delete(callback); |
| | | } |
| | | |
| | | notify(event, data = {}) { |
| | | this.subscribers.forEach( callback => { |
| | | try { |
| | | callback(event, data); |
| | | } catch (error) { |
| | | console.error('Subscriber error:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Clean up |
| | |
| | | // Destroy all stores |
| | | this.store.destroy(); |
| | | |
| | | this.subscribers.clear(); |
| | | // Clear all maps |
| | | this.fields.clear(); |
| | | this.selectedTerms.clear(); |
| | |
| | | * Initialize singleton |
| | | */ |
| | | document.addEventListener('DOMContentLoaded', function() { |
| | | window.jvbSelector = new TaxonomySelector(); |
| | | window.auth.subscribe((event) => { |
| | | if (event === 'auth-loaded') { |
| | | window.jvbSelector = new TaxonomySelector(); |
| | | } |
| | | }); |
| | | |
| | | }); |