From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve
---
assets/js/concise/TaxonomySelector.js | 884 ++++++++++++++++++++++++++++++++++++++++------------------
1 files changed, 613 insertions(+), 271 deletions(-)
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index b13e9ae..bf3af8f 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -8,9 +8,38 @@
this.error = window.jvbError;
this.index = -1;
- // DataStore instances per taxonomy
- this.stores = new Map();
- this.storeSubscriptions = new Map();
+ this.hasAutocomplete = false;
+ this.isInitializing = true;
+ this.taxonomiesToFetch = new Set();
+
+ 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();
@@ -21,13 +50,14 @@
this.currentConfig = null;
this.currentSingular = null;
this.currentPlural = null;
- this.activeStore = null;
// Modal state
this.disabled = false;
// Search debouncing
this.searchHandler = null;
+ this.autocompleteHandler = null;
+ this.isAutocompleteActive = false;
this.init();
}
@@ -39,62 +69,66 @@
this.initModal();
this.scanExistingFields();
this.initGlobalListeners();
- }
- /**
- * Get or create a DataStore for a taxonomy
- */
- getOrCreateStore(taxonomy) {
- if (!this.stores.has(taxonomy)) {
- const store = new window.jvbStore({
- name: `tax_${taxonomy}`,
- endpoint: 'terms',
- TTL: 3600000, // 1 hour cache
- filters: {
- taxonomy: taxonomy,
- page: 1,
- search: '',
- parent: 0
- }
- });
-
- // Subscribe to store events
- const unsubscribe = store.subscribe((event, data) => {
- this.handleStoreEvent(taxonomy, event, data);
- });
-
- this.stores.set(taxonomy, store);
- this.storeSubscriptions.set(taxonomy, unsubscribe);
+ if (this.hasAutocomplete && window.jvbTaxCreator) {
+ this.creator = new window.jvbTaxCreator(this);
}
-
- return this.stores.get(taxonomy);
+ this.store.subscribe(this.handleStoreEvent.bind(this));
+ // Complete initialization
+ this.isInitializing = false;
+ this.batchFetchTaxonomies();
}
/**
* Handle DataStore events
*/
- handleStoreEvent(taxonomy, event, data) {
- // Only process events for the active taxonomy in modal
- if (this.activeStore && this.activeStore.config.name === `tax_${taxonomy}`) {
- switch (event) {
- case 'items-loaded':
- case 'data-fetched':
- case 'data-cached':
- case 'stale-cache-used':
- this.handleTermsLoaded(data);
- break;
- case 'fetch-error':
- this.handleFetchError(data.error);
- break;
- case 'filters-changed':
- // Could trigger UI updates for active filters
- break;
- }
- }
+ handleStoreEvent(event, data) {
+ switch (event) {
+ case 'data-loaded':
+ 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];
- // Handle field-specific updates outside modal
- if (event === 'items-updated' || event === 'items-loaded') {
- this.updateFieldsForTaxonomy(taxonomy, data.items);
+ taxonomies.forEach(tax => {
+ this.updateFieldsForTaxonomy(tax);
+ });
+ }
+
+ // Only render if modal is open OR autocomplete active
+ if (this.modal?.open) {
+ this.handleTermsLoaded(data);
+ }
+
+ if (this.isAutocompleteActive && this.activeField) {
+ const field = this.fields.get(this.activeField);
+ const terms = data.data?.items || [];
+ const query = data.filters?.search || '';
+ this.showAutocompleteResults(field, terms, query);
+ this.isAutocompleteActive = false;
+ }
+ break;
+
+ case 'filters-changed':
+ if (this.modal?.open) {
+ this.showLoading();
+ }
+ break;
+
+ case 'fetch-error':
+ if (this.isAutocompleteActive && this.activeField) {
+ this.showAutocompleteError(this.activeField);
+ this.isAutocompleteActive = false;
+ }
+ this.handleFetchError(data.error);
+ break;
}
}
@@ -103,10 +137,12 @@
*/
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) {
@@ -115,9 +151,9 @@
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);
@@ -145,32 +181,53 @@
}
}
+
+ /**
+ * 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() {
- const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
+ scanExistingFields(container = null) {
+ if (!container) {
+ container = document.body;
+ }
+ const selectors = container.querySelectorAll('.field.taxonomy, .field.post');
+
selectors.forEach(selector => {
try {
this.registerField(selector);
@@ -190,14 +247,18 @@
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,
@@ -207,14 +268,21 @@
name: field.dataset.field,
maxSelection: parseInt(button.dataset.max) || 0,
canSearch: 'search' in button.dataset,
+ hasAutocomplete: 'autocomplete' in button.dataset,
+ autocompleteDropdown: field.querySelector('.autocomplete-dropdown')??false,
canCreate: 'creatable' in button.dataset,
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
};
+ if (!this.hasAutocomplete && config.hasAutocomplete) {
+ this.hasAutocomplete = true;
+ this.initAutocomplete();
+ }
+
// Parse initial selected values
const value = input.value.trim();
if (value !== '') {
@@ -224,10 +292,20 @@
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
- this.getOrCreateStore(config.taxonomy);
+ if (this.isInitializing) {
+ this.taxonomiesToFetch.add(config.taxonomy);
+ } else {
+ // this.store.setFilter('taxonomy', config.taxonomy);
+ }
// Initialize display for any pre-selected values
if (config.selectedTerms.size > 0) {
@@ -238,6 +316,46 @@
}
/**
+ * Register a filter button (simplified registration for feed blocks)
+ */
+ registerFilterButton(button, options = {}) {
+ const fieldId = this.createFieldId(button);
+ button.dataset.fieldId = fieldId;
+
+ if (options.buttonSelector) {
+ this.triggers.add(options.buttonSelector);
+ }
+
+ 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;
+ }
+
+ /**
* Create unique field ID
*/
createFieldId(field) {
@@ -252,47 +370,14 @@
const field = this.fields.get(fieldId);
if (!field || field.selectedTerms.size === 0) return;
- const store = this.getOrCreateStore(field.taxonomy);
const selectedIds = Array.from(field.selectedTerms);
- // Check store for cached terms first
- const cachedTerms = [];
- const needsFetch = [];
-
selectedIds.forEach(termId => {
- const term = store.getItem(termId);
+ const term = this.store.get(termId); // Changed from getItem
if (term) {
- cachedTerms.push(term);
- } else {
- needsFetch.push(termId);
+ this.addTermToDisplay(fieldId, term.id, term.name, term.path);
}
});
-
- // Display cached terms immediately
- cachedTerms.forEach(term => {
- this.addTermToDisplay(fieldId, term.id, term.name, term.path);
- });
-
- // Fetch missing terms if needed
- if (needsFetch.length > 0) {
- try {
- const response = await store.fetch('terms', {
- filters: {
- taxonomy: field.taxonomy,
- termIDs: needsFetch.join(',')
- }
- });
-
- if (response.terms) {
- response.terms.forEach(term => {
- store.setItem(term.id, term);
- this.addTermToDisplay(fieldId, term.id, term.name, term.path);
- });
- }
- } catch (error) {
- console.error('Failed to fetch missing terms:', error);
- }
- }
}
/**
@@ -318,7 +403,6 @@
this.modalInstance.subscribe((event, data) => {
switch (event) {
case 'modal-open':
- console.log(data);
this.openModal(data);
break;
case 'modal-close':
@@ -373,7 +457,7 @@
// Initialize intersection observer for infinite scroll
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
- if (entry.isIntersecting && this.activeStore) {
+ if (entry.isIntersecting) {
this.loadMoreTerms();
}
});
@@ -389,6 +473,30 @@
initGlobalListeners() {
document.addEventListener('click', this.handleClick.bind(this));
document.addEventListener('change', this.handleChange.bind(this));
+ if (this.hasAutocomplete) {
+ this.initAutocomplete();
+ }
+ }
+
+ initAutocomplete()
+ {
+ this.autocompleteHandler = window.debounce((e) => 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
}
/**
@@ -396,7 +504,8 @@
*/
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);
@@ -457,59 +566,54 @@
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);
- console.log('Current Taxonomy:',this.currentConfig.taxonomy);
- console.log('Labels: ',jvbSettings.labels[this.currentConfig.taxonomy]);
+ this.currentSingular = this.getSingular(this.currentConfig.taxonomy);
+ this.currentPlural = this.getPlural(this.currentConfig.taxonomy);
- this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single;
- this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
+ if (openModal) {
+ this.modalInstance.handleOpen();
+ }
- // Get or create store for this taxonomy
- this.activeStore = this.getOrCreateStore(this.currentConfig.taxonomy);
+ // 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) {
- this.currentConfig.selectedTerms.forEach(termId => {
- const term = this.activeStore.getItem(termId);
- if (term) {
- this.selectedTerms.set(termId, {
- id: termId,
- name: term.name,
- path: term.path
- });
- } else {
- // If not in store, create minimal entry
- this.selectedTerms.set(termId, {
- id: termId,
- name: `Term ${termId}`,
- path: `Term ${termId}`
- });
- }
- });
- }
+ 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
+ });
+ }
+ });
}
+
/**
* Handle clicks within modal
*/
@@ -570,6 +674,8 @@
name: `filter_${taxonomy}`,
maxSelection: 0, // No limit for filters
canSearch: true,
+ hasAutocomplete: false,
+ autocompleteDropdown: document.querySelector('.autocomplete-dropdown')??false,
canCreate: false, // Disable creation for filters
isRequired: false,
selectedTerms: new Set(preselected),
@@ -579,7 +685,7 @@
filterCallback: callback // Store the callback
});
- this.setActiveField(virtualFieldId);
+ this.setActiveField(virtualFieldId, true);
this.modalInstance.handleOpen();
}
@@ -587,37 +693,62 @@
* Open modal and initialize
*/
openModal() {
- if (!this.activeField || !this.currentConfig) {
- console.error('No active field set for modal');
+ if (!this.currentConfig) {
+ console.error('No active field set');
return;
}
- this.resetModalState();
- this.updateModalForTaxonomy();
-
- // Reset store filters to default state
- this.activeStore.clearFilters();
-
- // Set up search if enabled
- if (this.currentConfig.canSearch) {
- this.ui.search.input.focus();
- this.searchHandler = window.debounce(() => this.handleSearch(), 300);
- this.ui.search.input.addEventListener('input', this.searchHandler);
- }
-
// Initialize creator if available
- if (this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
+ if (!this.creator && this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
this.creator = new window.jvbTaxCreator(this);
}
- // Display current selections
+ // Update modal UI
+ this.updateModalForTaxonomy();
+
+ // Load selected terms display
this.updateModalSelections();
+ this.updateSelectionCount();
- // Start observing for infinite scroll
- this.observer.observe(this.ui.sentinel);
+ // Clear terms list and show loading
+ window.removeChildren(this.ui.termsList);
+ this.showLoading();
+ }
- // Fetch initial terms
- this.fetchCurrentTerms();
+ /**
+ * 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;
}
/**
@@ -627,15 +758,17 @@
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) {
- // Call the filter callback with selected terms
if (this.currentConfig.filterCallback) {
const selectedIds = Array.from(this.selectedTerms.keys());
this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy);
}
-
- // Clean up the virtual field
- this.fields.delete(this.activeField);
+ // this.fields.delete(this.activeField);
} else if (this.activeField) {
this.saveSelectionsToField(this.activeField);
}
@@ -645,11 +778,11 @@
this.ui.search.input.removeEventListener('input', this.searchHandler);
}
- if (this.creator) {
+ if (!this.hasAutocomplete && this.creator) {
delete this.creator;
}
- this.activeStore = null;
+ // Remove: this.activeStore = null;
this.activeField = null;
this.currentConfig = null;
}
@@ -887,45 +1020,170 @@
/**
* Handle search input
*/
- handleSearch() {
- const query = this.ui.searchInput.value.trim();
+ handleSearch(e) {
+ const query = e.target.value.trim();
- if (query.length >= 2 || query.length === 0) {
- // Reset pagination when searching
- this.activeStore.setFilter('page', 1);
- this.activeStore.setFilter('search', query);
+ // Clear existing debounce
+ if (this.searchHandler) {
+ clearTimeout(this.searchHandler);
+ }
+
+ this.searchHandler = setTimeout(() => {
+ // Single call - auto-fetches
+ this.store.setFilters({
+ search: query,
+ page: 1,
+ parent: query ? 0 : (this.store.filters.parent || 0)
+ });
window.removeChildren(this.ui.termsList);
+ }, 300);
+ }
- if (query.length >= 2) {
- this.showLoading();
- this.fetchCurrentTerms();
- } else if (query.length === 0) {
- // Clear search and reload
- this.showLoading();
- this.fetchCurrentTerms();
- }
- } else {
- this.hideLoading();
- this.showEmptyState('Enter at least 2 characters to search.');
+ async handleAutocomplete(e) {
+ if (!('autocomplete' in e.target.dataset)) {
+ return;
}
+
+ const fieldId = this.getFieldId(e.target);
+ const field = this.fields.get(fieldId);
+
+ if (!field) return;
+
+ // Store current value immediately (fixes fast typing issue)
+ const query = e.target.value.trim();
+ field.currentAutocompleteQuery = query;
+
+ if (query.length < 2) {
+ if (field.autocompleteDropdown) {
+ field.autocompleteDropdown.hidden = true;
+ }
+ this.isAutocompleteActive = false;
+ return;
+ }
+
+ this.activeField = fieldId;
+ this.isAutocompleteActive = true;
+
+ if (field.autocompleteDropdown) {
+ field.autocompleteDropdown.hidden = false;
+ }
+
+ this.store.setFilters({
+ taxonomy: field.taxonomy,
+ search: query,
+ page: 1
+ });
+ }
+
+ cleanupAutocomplete(e) {
+ if (!('autocomplete' in e.target.dataset)) {
+ return;
+ }
+
+ const fieldId = this.getFieldId(e.target);
+ const field = this.fields.get(fieldId);
+
+ if (!field) return;
+
+ if (this.creator) {
+ delete this.creator;
+ }
+ }
+
+ showAutocompleteError(fieldId) {
+
+ const field = this.fields.get(fieldId);
+ if (!field) {
+ return;
+ }
+ if (!field.config.autocompleteDropdown) {
+ field.config.autocompleteDropdown = field.element.querySelector('.autocomplete-dropdown');
+ }
+ const dropdown = field.config.autocompleteDropdown;
+ if (dropdown) {
+ window.removeChildren(dropdown);
+ this.showEmptyState('Hmmm... something went wrong', dropdown);
+ }
+ }
+
+ showAutocompleteResults(field, terms, query) {
+ if (!field || !field.autocompleteDropdown) {
+ return;
+ }
+
+ const dropdown = field.autocompleteDropdown;
+ window.removeChildren(dropdown);
+
+ if (terms.length === 0) {
+ this.showEmptyState('No items found.', dropdown);
+ } else {
+ terms.forEach(term => {
+ const element = this.createAutocompleteTermElement(field, term);
+ if (element) {
+ dropdown.appendChild(element);
+ }
+ });
+ }
+
+ // Use stored current query instead of debounced one
+ const currentQuery = field.currentAutocompleteQuery || query;
+ if (field.canCreate && currentQuery && window.jvbTaxCreator) {
+ const createOption = this.createNewTermOption(currentQuery);
+ dropdown.appendChild(createOption);
+ }
+
+ dropdown.hidden = false;
+ }
+
+ createNewTermOption(query) {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'autocomplete-item create-term';
+ button.dataset.query = query;
+ button.innerHTML = `<strong>Create:</strong> "${query}"`;
+
+ return button;
+ }
+
+ createAutocompleteTermElement(field, term) {
+ const item = document.createElement('button');
+ item.type = 'button';
+ item.className = 'autocomplete-item';
+ item.dataset.id = term.id;
+ item.dataset.name = term.name;
+ item.dataset.path = term.path || term.name;
+ item.textContent = term.path || term.name;
+
+ item.addEventListener('click', () => {
+ // Add term to field
+ field.selectedTerms.add(parseInt(term.id));
+ this.addTermToDisplay(field.id, term.id, term.name, term.path);
+
+ // Update input
+ field.input.value = Array.from(field.selectedTerms).join(',');
+ field.input.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // Clear and hide dropdown
+ field.autocompleteDropdown.hidden = true;
+ const input = field.container.querySelector('input[data-autocomplete]');
+ if (input) input.value = '';
+ });
+
+ return item;
}
/**
* Navigate to parent term
*/
navigateToParent() {
- const currentParent = this.activeStore.filters.parent || 0;
-
- // Find parent of current parent (could enhance this with breadcrumb tracking)
- this.activeStore.setFilter('parent', 0);
- this.activeStore.setFilter('page', 1);
+ // Store handles fetch automatically
+ this.store.setFilters({
+ parent: 0,
+ page: 1
+ });
window.removeChildren(this.ui.termsList);
- this.showLoading();
- this.fetchCurrentTerms();
-
- // Update breadcrumbs
this.ui.breadcrumbs.back.hidden = true;
}
@@ -933,14 +1191,13 @@
* Navigate to child term
*/
navigateToChild(termId, termName) {
- this.activeStore.setFilter('parent', termId);
- this.activeStore.setFilter('page', 1);
+ // Store handles fetch automatically
+ this.store.setFilters({
+ parent: termId,
+ page: 1
+ });
window.removeChildren(this.ui.termsList);
- this.showLoading();
- this.fetchCurrentTerms();
-
- // Update breadcrumbs
this.updateBreadcrumbs(termId, termName);
this.ui.breadcrumbs.back.hidden = false;
}
@@ -951,43 +1208,33 @@
navigateToPath(pathLevel) {
const parentId = parseInt(pathLevel.dataset.id) || 0;
- this.activeStore.setFilter('parent', parentId);
- this.activeStore.setFilter('page', 1);
+ // Store handles fetch automatically
+ this.store.setFilters({
+ parent: parentId,
+ page: 1
+ });
window.removeChildren(this.ui.termsList);
- this.showLoading();
- this.fetchCurrentTerms();
-
- // Update breadcrumbs to this level
- // You'd need to track the full path to properly implement this
this.ui.breadcrumbs.back.hidden = parentId === 0;
}
/**
- * Fetch terms using current store filters
- */
- fetchCurrentTerms() {
- if (!this.activeStore) return;
-
- this.showLoading();
- this.activeStore.fetch();
- }
-
- /**
* Load more terms (pagination)
*/
loadMoreTerms() {
- if (!this.activeStore) return;
-
- const currentPage = this.activeStore.filters.page || 1;
- this.activeStore.setFilter('page', currentPage + 1);
- // fetch() will be called automatically by setFilter
+ 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);
}
@@ -999,38 +1246,25 @@
return;
}
- // Update breadcrumbs if needed
- const currentParent = this.activeStore.filters.parent || 0;
+ const currentParent = this.store.filters.parent || 0;
this.ui.breadcrumbs.back.hidden = currentParent === 0;
+ const fragment = document.createDocumentFragment();
terms.forEach(term => {
- // Check if we have a cached DOM element
- const cachedElement = this.activeStore.getDOMElement(term.id, 'list-item');
+ const element = this.createTermElement({
+ id: parseInt(term.id),
+ name: term.name,
+ hasChildren: term.hasChildren,
+ path: term.path || null,
+ show: showPath
+ });
- if (cachedElement) {
- // Update checkbox state if needed
- const checkbox = cachedElement.querySelector('input[type="checkbox"]');
- if (checkbox) {
- checkbox.checked = this.selectedTerms.has(term.id);
- checkbox.disabled = !checkbox.checked && this.disabled;
- }
- this.ui.termsList.appendChild(cachedElement);
- } else {
- // Create new element and cache it
- const element = this.createTermElement({
- id: parseInt(term.id),
- name: term.name,
- hasChildren: term.hasChildren,
- path: term.path || null,
- show: showPath
- });
-
- if (element) {
- this.activeStore.storeDOMElement(term.id, 'list-item', element);
- this.ui.termsList.appendChild(element);
- }
+ if (element) {
+ fragment.appendChild(element);
}
});
+
+ this.ui.termsList.appendChild(fragment);
}
/**
@@ -1115,8 +1349,8 @@
this.ui.loading.loading.hidden = false;
this.modal.classList.add('loading');
- const searchQuery = this.activeStore?.filters?.search || '';
- const currentParent = this.activeStore?.filters?.parent || 0;
+ const searchQuery = this.store?.filters?.search || '';
+ const currentParent = this.store?.filters?.parent || 0;
let message = searchQuery !== '' ?
`searching for "${searchQuery}" items` :
@@ -1146,14 +1380,17 @@
/**
* Show empty state message
*/
- showEmptyState(message = 'No items found.') {
+ showEmptyState(message = 'No items found.', container = null) {
+ if (!container) {
+ container = this.ui.termsList;
+ }
const emptyElement = window.getTemplate('noResults').cloneNode(true);
if (message && emptyElement.querySelector('span')) {
emptyElement.querySelector('span').textContent = message;
}
- this.ui.termsList.appendChild(emptyElement);
+ container.appendChild(emptyElement);
}
/**
@@ -1171,6 +1408,117 @@
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
@@ -1183,16 +1531,12 @@
// Clear intervals and cleanup
this.observer?.disconnect();
- // Unsubscribe from all stores
- this.storeSubscriptions.forEach(unsubscribe => unsubscribe());
-
// Destroy all stores
- this.stores.forEach(store => store.destroy());
+ this.store.destroy();
+ this.subscribers.clear();
// Clear all maps
this.fields.clear();
- this.stores.clear();
- this.storeSubscriptions.clear();
this.selectedTerms.clear();
}
}
@@ -1201,7 +1545,5 @@
* Initialize singleton
*/
document.addEventListener('DOMContentLoaded', function() {
- if (!window.jvbSelector) {
- window.jvbSelector = new TaxonomySelector();
- }
+ window.jvbSelector = new TaxonomySelector();
});
--
Gitblit v1.10.0