From 552d48a1424417da160c4952650ea6f4a3d7bafa Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 20:18:23 +0000
Subject: [PATCH] =Taxonomy Selector and Creator refactor
---
assets/js/concise/TaxonomySelector.js | 1433 +++++++++++++++++++++++++++--------------------------------
1 files changed, 661 insertions(+), 772 deletions(-)
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index b13e9ae..b411d11 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,6 +1,6 @@
/**
- * Centralized Taxonomy Selector with DataStore Integration
- * Handles all taxonomy selection fields using DataStore for state management
+ * TaxonomySelector - Streamlined version
+ * Manages taxonomy selection fields with DataStore integration
*/
class TaxonomySelector {
constructor() {
@@ -8,105 +8,136 @@
this.error = window.jvbError;
this.index = -1;
- // DataStore instances per taxonomy
- this.stores = new Map();
- this.storeSubscriptions = new Map();
+ this.isInitializing = true;
+ this.taxonomiesToFetch = new Set();
+ this.subscribers = new Set();
- // Central field management
+ // Register DataStore
+ 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,
+ filters: {
+ taxonomy: '',
+ page: 1,
+ search: '',
+ parent: 0
+ },
+ required: 'taxonomy',
+ delayFetch: true,
+ });
+ this.store = store.terms;
+
+ // Field management
this.fields = new Map();
- this.selectedTerms = new Map(); // Current modal selection
+ this.selectedTerms = new Map(); // Current modal selection
- // Current modal context
+ // Modal context
this.activeField = null;
this.currentConfig = null;
- this.currentSingular = null;
- this.currentPlural = null;
- this.activeStore = null;
-
- // Modal state
this.disabled = false;
- // Search debouncing
- this.searchHandler = null;
+ // Search contexts
+ this.searchContexts = new Map();
this.init();
}
- /**
- * Initialize the selector
- */
init() {
this.initModal();
this.scanExistingFields();
this.initGlobalListeners();
+
+ // Initialize creator if needed
+ if (this.needsCreator() && window.jvbTaxCreator) {
+ this.creator = new window.jvbTaxCreator(this);
+ }
+
+ this.store.subscribe(this.handleStoreEvent.bind(this));
+
+ this.isInitializing = false;
+ this.batchFetchTaxonomies();
}
- /**
- * 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
+ needsCreator() {
+ return Array.from(this.fields.values()).some(field =>
+ field.canCreate || field.hasAutocomplete
+ );
+ }
+
+ /***********************************************************************
+ * DATASTORE EVENT HANDLING
+ ***********************************************************************/
+
+ handleStoreEvent(event, data) {
+ const handlers = {
+ 'data-loaded': () => this.handleDataLoaded(data),
+ 'filters-changed': () => this.handleFiltersChanged(data),
+ 'fetch-error': () => this.handleFetchError(data.error),
+ };
+
+ handlers[event]?.();
+ }
+
+ handleDataLoaded(data) {
+ const taxonomy = this.store.filters.taxonomy;
+
+ // Update field states for affected taxonomies
+ if (taxonomy) {
+ const taxonomies = taxonomy.includes(',')
+ ? taxonomy.split(',').map(t => t.trim())
+ : [taxonomy];
+
+ taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
+ }
+
+ // Initialize displays on first load
+ if (this.isInitializing) {
+ this.fields.forEach((config, fieldId) => {
+ if (config.selectedTerms.size > 0) {
+ this.initFieldDisplay(fieldId);
}
});
-
- // 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);
}
- return this.stores.get(taxonomy);
+ // Render based on context
+ this.renderSearchResults(data);
}
- /**
- * 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;
- }
- }
+ renderSearchResults(data) {
+ const context = this.getActiveSearchContext();
- // Handle field-specific updates outside modal
- if (event === 'items-updated' || event === 'items-loaded') {
- this.updateFieldsForTaxonomy(taxonomy, data.items);
+ if (context === 'modal') {
+ this.renderModalResults(data);
+ } else if (context === 'autocomplete') {
+ this.renderAutocompleteResults(data);
}
}
- /**
- * Handle loaded terms from DataStore
- */
- handleTermsLoaded(data) {
+ getActiveSearchContext() {
+ if (this.modal?.open) return 'modal';
+ if (this.activeField && this.searchContexts.has(this.activeField)) {
+ return this.searchContexts.get(this.activeField);
+ }
+ return null;
+ }
+
+ renderModalResults(data) {
this.hideLoading();
- const terms = data.data?.items || [];
- const pagination = data.data?.pagination || {};
- const isSearch = data.filters?.search && data.filters.search.length > 0;
- const append = data.filters?.page > 1;
+ const terms = this.store.getFiltered();
+ const response = this.store.lastResponse?.page || {};
+ const isSearch = data.filters?.search?.length > 0;
+ const append = response.page > 1;
+
+ this.notify('terms-loaded', { terms, filters: data.filters });
if (terms.length === 0) {
if (!append) {
@@ -115,91 +146,96 @@
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);
}
}
- // Announce to screen readers
this.a11y?.announce(terms.length, append);
}
- /**
- * Handle fetch errors
- */
- handleFetchError(error) {
- console.error('Taxonomy fetch error:', error);
- this.hideLoading();
+ renderAutocompleteResults(data) {
+ const field = this.fields.get(this.activeField);
+ if (!field?.autocompleteDropdown) return;
- if (this.error?.log) {
- this.error.log(error, {
- component: 'TaxonomySelector',
- action: 'fetchTerms'
- }, () => this.fetchCurrentTerms());
- } else {
- this.showEmptyState('Error loading terms. Please try again.');
+ const terms = this.store.getFiltered();
+ const query = data.filters?.search || '';
+
+ this.showAutocompleteResults(field, terms, query);
+ this.searchContexts.delete(this.activeField);
+ }
+
+ handleFiltersChanged(data) {
+ if (this.modal?.open) {
+ this.showLoading();
}
}
- /**
- * 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;
- }
- }
- });
- }
+ handleFetchError(error) {
+ this.hideLoading();
+
+ const context = this.getActiveSearchContext();
+
+ if (context === 'autocomplete') {
+ this.showAutocompleteError(this.activeField);
+ this.searchContexts.delete(this.activeField);
+ } else {
+ this.handleError(error, 'fetch');
+ }
+ }
+
+ /***********************************************************************
+ * FIELD MANAGEMENT
+ ***********************************************************************/
+
+ updateFieldsForTaxonomy(taxonomy) {
+ this.getFieldsForTaxonomy(taxonomy).forEach(field => {
+ this.updateFieldButtonState(field.id);
});
}
- /**
- * Scan page for existing taxonomy fields and register them
- */
- scanExistingFields() {
- const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
- selectors.forEach(selector => {
+ updateFieldButtonState(fieldId) {
+ const field = this.fields.get(fieldId);
+ if (!field) return;
+
+ 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.getLabel(field.taxonomy, 'single')} available`
+ : `Select ${this.getLabel(field.taxonomy, 'plural')}`;
+ }
+ }
+
+ getFieldsForTaxonomy(taxonomy) {
+ return Array.from(this.fields.values())
+ .filter(field => field.taxonomy === taxonomy);
+ }
+
+ scanExistingFields(container = document.body) {
+ container.querySelectorAll('.field.taxonomy, .field.post').forEach(selector => {
try {
this.registerField(selector);
} catch (error) {
- this.error.log(error, {
- component: 'TaxonomySelector',
- action: 'scanExistingFields',
- container: selector.dataset.name
- });
+ this.handleError(error, 'scanExistingFields', selector.dataset.name);
}
});
}
- /**
- * Register a taxonomy field
- */
- registerField(field, options = {}) {
- let input = field.querySelector('input[type=hidden]');
- if (!input) {
- return;
- }
+ registerField(field) {
+ const input = field.querySelector('input[type=hidden]');
+ if (!input) 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');
+ const fieldId = this.createFieldId(field);
+ field.dataset.fieldId = fieldId;
- let config = {
+ const button = field.querySelector('button.taxonomy-toggle');
+ const config = {
id: fieldId,
input: input,
container: field,
@@ -207,29 +243,32 @@
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') || null,
canCreate: 'creatable' in button.dataset,
isRequired: 'required' in button.dataset,
selectedTerms: new Set(),
toggle: button,
selectedContainer: field.querySelector('.selected-items'),
- ...options
};
- // Parse initial selected values
+ // Parse initial values
const value = input.value.trim();
- if (value !== '') {
- const selectedIds = value.split(',')
+ if (value) {
+ value.split(',')
.map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id));
- selectedIds.forEach(id => config.selectedTerms.add(id));
+ .filter(id => !isNaN(id))
+ .forEach(id => config.selectedTerms.add(id));
}
this.fields.set(fieldId, config);
- // Ensure store exists for this taxonomy
- this.getOrCreateStore(config.taxonomy);
+ // Queue for batch fetch
+ if (this.isInitializing) {
+ this.taxonomiesToFetch.add(config.taxonomy);
+ }
- // Initialize display for any pre-selected values
+ // Initialize display
if (config.selectedTerms.size > 0) {
this.initFieldDisplay(fieldId);
}
@@ -237,71 +276,29 @@
return fieldId;
}
- /**
- * Create unique field ID
- */
createFieldId(field) {
this.index++;
return 'selector-' + this.index;
}
- /**
- * Initialize display for a field with existing values
- */
async initFieldDisplay(fieldId) {
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);
+ Array.from(field.selectedTerms).forEach(termId => {
+ const term = this.store.get(termId);
if (term) {
- cachedTerms.push(term);
- } else {
- needsFetch.push(termId);
+ this.addTermDisplay(termId, term.name, term.path, 'field', fieldId);
}
});
-
- // 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);
- }
- }
}
- /**
- * Initialize modal elements
- */
- initModal() {
- this.modalID = 'dialog#jvb-selector';
- this.modal = document.querySelector(this.modalID);
+ /***********************************************************************
+ * MODAL INITIALIZATION
+ ***********************************************************************/
+ initModal() {
+ this.modal = document.querySelector('dialog#jvb-selector');
if (!this.modal) {
console.warn('Taxonomy selector modal not found');
return;
@@ -309,37 +306,24 @@
this.initModalElements();
- // Initialize modal instance
this.modalInstance = new window.jvbModal(this.modal, {
- handleForm: false,
- save: null,
- open: null
+ handleForm: false
});
- this.modalInstance.subscribe((event, data) => {
- switch (event) {
- case 'modal-open':
- console.log(data);
- this.openModal(data);
- break;
- case 'modal-close':
- this.closeModal(data);
- break;
- }
+
+ this.modalInstance.subscribe((event) => {
+ if (event === 'modal-open') this.openModal();
+ if (event === 'modal-close') this.closeModal();
});
}
- /**
- * Initialize modal element references
- */
initModalElements() {
- this.selectors = {
+ const selectors = {
search: {
input: '[type=search]',
- clear: '.clear-search',
container: '.search-wrapper'
},
- termsList: '.items-container',
- termsWrap: '.items-wrap',
+ termsList: '.items-container',
+ termsWrap: '.items-wrap',
breadcrumbs: {
nav: 'nav.term-navigation',
back: '.back-to-parent',
@@ -352,28 +336,23 @@
sentinel: '.scroll-sentinel',
modal: {
title: '#modal-title',
- content: '.modal-content'
},
create: {
details: '.create-new-term',
- parent: '#select_parent',
summary: '.create-new-term summary',
- name: '#term_name',
- button: '.submit-term',
label: {
name: '[for=term_name]',
parent: '[for=select_parent]'
}
- },
- favouriteTerms: '.favourite-terms'
- }
+ }
+ };
- this.ui = window.uiFromSelectors(this.selectors);
+ this.ui = window.uiFromSelectors(selectors);
- // Initialize intersection observer for infinite scroll
+ // Initialize infinite scroll observer
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
- if (entry.isIntersecting && this.activeStore) {
+ if (entry.isIntersecting) {
this.loadMoreTerms();
}
});
@@ -383,27 +362,29 @@
});
}
- /**
- * Set up global event delegation
- */
+ /***********************************************************************
+ * GLOBAL EVENT LISTENERS
+ ***********************************************************************/
+
initGlobalListeners() {
document.addEventListener('click', this.handleClick.bind(this));
document.addEventListener('change', this.handleChange.bind(this));
+ document.addEventListener('input', this.handleInput.bind(this));
+ document.addEventListener('focus', this.handleFocus.bind(this), true);
+ document.addEventListener('blur', this.handleBlur.bind(this), true);
}
- /**
- * Handle global click events
- */
handleClick(e) {
- // Handle taxonomy toggle buttons
- const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
- if (toggleButton) {
+ // Toggle button
+ if (window.targetCheck(e, '.taxonomy-toggle')) {
e.preventDefault();
- this.handleToggleClick(toggleButton);
+ const fieldId = this.getFieldId(e.target);
+ const field = this.fields.get(fieldId);
+ if (field) this.setActiveField(fieldId, true);
return;
}
- // Handle remove selected term buttons
+ // Remove selected term
const removeButton = window.targetCheck(e, 'button.remove-item');
if (removeButton && e.target.closest('.jvb-selector')) {
const fieldId = this.getFieldId(removeButton);
@@ -412,25 +393,20 @@
return;
}
- // Handle modal close button
+ // Modal close
if (e.target.matches('.modal-close')) {
- if (this.modalInstance) {
- this.modalInstance.handleClose();
- }
+ this.modalInstance?.handleClose();
return;
}
- // Handle clicks within the modal
- if (this.modal && this.modal.contains(e.target)) {
+ // Modal clicks
+ if (this.modal?.contains(e.target)) {
this.handleModalClick(e);
}
}
- /**
- * Handle global change events
- */
handleChange(e) {
- // Handle hidden input changes for taxonomy fields
+ // Hidden input changes
const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
if (taxonomyField && e.target.type === 'hidden') {
const fieldId = this.getFieldId(e.target);
@@ -438,245 +414,205 @@
return;
}
- // Handle modal changes
- if (this.modal && this.modal.contains(e.target)) {
+ // Modal checkboxes
+ if (this.modal?.contains(e.target)) {
this.handleModalChange(e);
}
}
- /**
- * Handle toggle button click
- */
- handleToggleClick(toggle) {
- try {
- const fieldId = this.getFieldId(toggle);
+ handleInput(e) {
+ // Modal search
+ if (this.modal?.contains(e.target) && e.target.type === 'search') {
+ this.performSearch(e.target.value.trim(), 'modal');
+ return;
+ }
+
+ // Autocomplete
+ if ('autocomplete' in e.target.dataset) {
+ const fieldId = this.getFieldId(e.target);
+ const field = this.fields.get(fieldId);
+ if (field?.hasAutocomplete) {
+ this.performSearch(e.target.value.trim(), 'autocomplete', fieldId);
+ }
+ }
+ }
+
+ handleFocus(e) {
+ if (!('autocomplete' in e.target.dataset)) return;
+
+ const fieldId = this.getFieldId(e.target);
+ const field = this.fields.get(fieldId);
+
+ if (field?.hasAutocomplete) {
+ this.preloadTaxonomy(field.taxonomy);
+ }
+ }
+
+ handleBlur(e) {
+ if (!('autocomplete' in e.target.dataset)) return;
+
+ setTimeout(() => {
+ const fieldId = this.getFieldId(e.target);
const field = this.fields.get(fieldId);
- if (!field) {
- console.error('Field not found for toggle:', fieldId);
+ if (field?.autocompleteDropdown) {
+ field.autocompleteDropdown.hidden = true;
+ }
+
+ this.searchContexts.delete(fieldId);
+ }, 200);
+ }
+
+ /***********************************************************************
+ * UNIFIED SEARCH
+ ***********************************************************************/
+
+ performSearch(query, context = 'modal', fieldId = null) {
+ const field = context === 'autocomplete'
+ ? this.fields.get(fieldId)
+ : this.currentConfig;
+
+ if (!field) return;
+
+ // Autocomplete validation
+ if (context === 'autocomplete') {
+ field.currentAutocompleteQuery = query;
+
+ if (query.length < 2) {
+ if (field.autocompleteDropdown) {
+ field.autocompleteDropdown.hidden = true;
+ }
return;
}
- this.setActiveField(fieldId);
- this.modalInstance.handleOpen();
+ this.searchContexts.set(fieldId, 'autocomplete');
+ this.activeField = fieldId;
- } catch (error) {
- console.error('Error handling toggle click:', error);
- this.error?.handleError(error, {
- component: 'TaxonomySelector',
- action: 'handleToggleClick'
- });
+ if (field.autocompleteDropdown) {
+ field.autocompleteDropdown.hidden = false;
+ }
}
+
+ // Debounced search
+ window.debouncer.schedule(
+ `taxonomy-search-${context}-${fieldId || 'modal'}`,
+ async () => {
+ await this.store.setFilters({
+ taxonomy: field.taxonomy,
+ search: query,
+ page: 1,
+ parent: query ? 0 : (this.store.filters.parent || 0)
+ });
+
+ if (context === 'modal') {
+ window.removeChildren(this.ui.termsList);
+ }
+ },
+ 300
+ );
}
- /**
- * Set the active field for modal operations
- */
- setActiveField(fieldId) {
+ /***********************************************************************
+ * MODAL OPERATIONS
+ ***********************************************************************/
+
+ 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]);
+ if (openModal) {
+ this.modalInstance.handleOpen();
+ }
- this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single;
- this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
+ this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
- // Get or create store for this taxonomy
- this.activeStore = this.getOrCreateStore(this.currentConfig.taxonomy);
-
- // Clear modal selection state
+ // Reset 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}`
- });
- }
- });
- }
+ // Copy field selections to modal
+ 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
- */
handleModalClick(e) {
if (window.targetCheck(e, '.remove-item')) {
- let selectedItem = window.targetCheck(e, '.selected-item');
+ const selectedItem = window.targetCheck(e, '.selected-item');
if (selectedItem) {
this.removeSelectedTermFromModal(selectedItem.dataset.id);
}
} else if (window.targetCheck(e, '.back-to-parent')) {
this.navigateToParent();
} else if (window.targetCheck(e, '.toggle-children')) {
- let termItem = e.target.closest('li');
+ const termItem = e.target.closest('li');
this.navigateToChild(
parseInt(termItem.dataset.id),
termItem.querySelector('.term-name').textContent
);
} else if (window.targetCheck(e, '.path-level')) {
- let pathLevel = window.targetCheck(e, '.path-level');
- this.navigateToPath(pathLevel);
+ const pathLevel = window.targetCheck(e, '.path-level');
+ this.navigateToPath(parseInt(pathLevel.dataset.id) || 0);
}
}
- /**
- * Handle changes within modal (checkboxes)
- */
handleModalChange(e) {
- if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
- e.preventDefault();
- e.stopPropagation();
+ if (e.target.type !== 'checkbox') return;
- const termId = parseInt(e.target.closest('li').dataset.id);
- const label = e.target.closest('li').querySelector('label');
+ e.preventDefault();
+ e.stopPropagation();
- if (e.target.checked) {
- this.addSelectedTermToModal(termId, label.title, label.dataset.path);
- } else {
- this.removeSelectedTermFromModal(termId);
- }
+ const termId = parseInt(e.target.closest('li').dataset.id);
+ const label = e.target.closest('li').querySelector('label');
+
+ if (e.target.checked) {
+ this.addSelectedTermToModal(termId, label.title, label.dataset.path);
+ } else {
+ this.removeSelectedTermFromModal(termId);
}
}
- /**
- * Open modal for filtering (without a field)
- * @param {string} taxonomy - The taxonomy to filter by
- * @param {Function} callback - Callback when terms are selected
- * @param {Array} preselected - Array of term IDs already selected
- */
- openForFilter(taxonomy, callback, preselected = []) {
- // Create a temporary virtual field config
- const virtualFieldId = `filter-${taxonomy}-${Date.now()}`;
-
- this.fields.set(virtualFieldId, {
- id: virtualFieldId,
- input: null, // No input for filter mode
- container: null,
- taxonomy: taxonomy,
- name: `filter_${taxonomy}`,
- maxSelection: 0, // No limit for filters
- canSearch: true,
- canCreate: false, // Disable creation for filters
- isRequired: false,
- selectedTerms: new Set(preselected),
- toggle: null,
- selectedContainer: null,
- isFilterMode: true, // Flag for filter mode
- filterCallback: callback // Store the callback
- });
-
- this.setActiveField(virtualFieldId);
- this.modalInstance.handleOpen();
- }
-
- /**
- * 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) {
- this.creator = new window.jvbTaxCreator(this);
- }
-
- // Display current selections
+ this.updateModalUI();
this.updateModalSelections();
- // Start observing for infinite scroll
- this.observer.observe(this.ui.sentinel);
-
- // Fetch initial terms
- this.fetchCurrentTerms();
+ window.removeChildren(this.ui.termsList);
+ this.showLoading();
}
- /**
- * Close modal and save selections
- */
closeModal() {
this.observer.unobserve(this.ui.sentinel);
window.removeChildren(this.ui.termsList);
- 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);
- }
+ this.notify('selected-terms', {
+ terms: this.selectedTerms,
+ taxonomy: this.currentConfig.taxonomy
+ });
- // Clean up the virtual field
- this.fields.delete(this.activeField);
- } else if (this.activeField) {
+ if (this.activeField) {
this.saveSelectionsToField(this.activeField);
}
- // Cleanup
- if (this.currentConfig?.canSearch && this.searchHandler) {
- this.ui.search.input.removeEventListener('input', this.searchHandler);
- }
-
- if (this.creator) {
- delete this.creator;
- }
-
- this.activeStore = null;
this.activeField = null;
this.currentConfig = null;
}
- /**
- * Reset modal state
- */
- resetModalState() {
- this.disabled = false;
+ updateModalUI() {
+ const singular = this.getLabel(this.currentConfig.taxonomy, 'single');
+ const plural = this.getLabel(this.currentConfig.taxonomy, 'plural');
- window.removeChildren(this.ui.termsList);
- window.removeChildren(this.ui.selectedTerms);
- this.ui.search.input.value = '';
-
- // Clear navigation breadcrumbs
- window.removeChildren(this.ui.breadcrumbs.nav);
- this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back);
- this.ui.breadcrumbs.back.hidden = true;
- }
-
- /**
- * Update modal content for current taxonomy
- */
- updateModalForTaxonomy() {
- if (!this.currentConfig) return;
-
- this.ui.modal.title.textContent = `Select ${this.currentPlural}`;
+ this.ui.modal.title.textContent = `Select ${plural}`;
if (this.ui.search.container) {
this.ui.search.container.style.display = this.currentConfig.canSearch ? 'block' : 'none';
@@ -687,185 +623,126 @@
this.ui.create.details.hidden = !this.currentConfig.canCreate;
if (this.ui.create.summary) {
- this.ui.create.summary.textContent = `Add new ${this.currentSingular}`;
+ this.ui.create.summary.textContent = `Add new ${singular}`;
}
if (this.ui.create.label.name) {
- this.ui.create.label.name.textContent = `Name this ${this.currentSingular}`;
+ this.ui.create.label.name.textContent = `Name this ${singular}`;
}
if (this.ui.create.label.parent) {
this.ui.create.label.parent.textContent = `Nest it under`;
}
-
- if (this.ui.create.parent) {
-
- }
}
- const openMessage = `Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;
- this.a11y?.announce(openMessage);
+ this.a11y?.announce(`Opened ${singular} selection. Choose from checkboxes or search to filter results.`);
}
- /**
- * Update modal selections display
- */
updateModalSelections() {
window.removeChildren(this.ui.selectedTerms);
this.selectedTerms.forEach((termData, id) => {
- this.addTermToModalDisplay(id, termData.name, termData.path);
+ this.addTermDisplay(id, termData.name, termData.path, 'modal');
});
this.checkSelectionLimits();
}
- /**
- * Add selected term to modal
- */
addSelectedTermToModal(id, name, path) {
- this.selectedTerms.set(id, {
- id: id,
- name: name,
- path: path
- });
+ this.selectedTerms.set(id, { id, name, path });
- this.addTermToModalDisplay(id, name, path);
+ this.addTermDisplay(id, name, path, 'modal');
this.checkSelectionLimits();
- // Check the corresponding checkbox
const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
- if (checkbox) {
- checkbox.checked = true;
- }
+ if (checkbox) checkbox.checked = true;
}
- /**
- * Remove selected term from modal
- */
removeSelectedTermFromModal(id) {
this.selectedTerms.delete(parseInt(id));
- // Remove from modal display
const selectedItem = this.ui.selectedTerms.querySelector(`[data-id="${id}"]`);
- if (selectedItem) {
- selectedItem.remove();
- }
+ if (selectedItem) selectedItem.remove();
- // Uncheck the corresponding checkbox
const checkbox = this.ui.termsList.querySelector(`input[value="${id}"]`);
- if (checkbox) {
- checkbox.checked = false;
- }
+ if (checkbox) checkbox.checked = false;
this.checkSelectionLimits();
}
- /**
- * Add term to modal display
- */
- addTermToModalDisplay(id, name, path) {
- const item = window.getTemplate('selectedTerm').cloneNode(true);
- item.dataset.id = id;
- item.dataset.path = path;
- item.dataset.name = name;
- item.dataset.taxonomy = this.currentConfig.taxonomy;
- item.querySelector('span').textContent = path;
- item.querySelector('button').title = `Remove ${name}`;
-
- this.ui.selectedTerms.appendChild(item);
- }
-
- /**
- * Check selection limits and disable/enable checkboxes
- */
checkSelectionLimits() {
if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
return;
}
this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
- this.setCheckboxes(this.disabled);
- }
- /**
- * Set checkbox disabled state
- */
- setCheckboxes(disabled) {
this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
if (!checkbox.checked) {
- checkbox.disabled = disabled;
+ checkbox.disabled = this.disabled;
}
});
}
- /**
- * Save modal selections to field
- */
saveSelectionsToField(fieldId) {
const field = this.fields.get(fieldId);
if (!field) return;
- // Clear current field selections
field.selectedTerms.clear();
window.removeChildren(field.selectedContainer);
- // Add modal selections to field
this.selectedTerms.forEach((termData, id) => {
field.selectedTerms.add(id);
- this.addTermToDisplay(fieldId, id, termData.name, termData.path);
+ this.addTermDisplay(id, termData.name, termData.path, 'field', fieldId);
});
- // Update hidden input
- const selectedIds = Array.from(field.selectedTerms);
- field.input.value = selectedIds.join(',');
+ field.input.value = Array.from(field.selectedTerms).join(',');
field.input.dispatchEvent(new Event('change', { bubbles: true }));
}
- /**
- * Remove selected term from field
- */
+ /***********************************************************************
+ * TERM DISPLAY
+ ***********************************************************************/
+
+ addTermDisplay(termId, termName, termPath, context = 'field', fieldId = null) {
+ const config = context === 'field'
+ ? this.fields.get(fieldId)
+ : this.currentConfig;
+
+ const container = context === 'field'
+ ? config.selectedContainer
+ : this.ui.selectedTerms;
+
+ if (container.querySelector(`[data-id="${termId}"]`)) return;
+
+ const item = window.getTemplate('selectedTerm');
+ item.dataset.id = termId;
+ item.dataset.path = termPath;
+ item.dataset.name = termName;
+ item.dataset.taxonomy = config.taxonomy;
+ item.querySelector('.item-name').textContent = termPath;
+ item.querySelector('button').title = `Remove ${termName}`;
+
+ container.appendChild(item);
+
+ if (context === 'modal') {
+ const checkbox = this.ui.termsList.querySelector(`input[value="${termId}"]`);
+ if (checkbox) checkbox.checked = true;
+ }
+ }
+
removeSelectedTerm(fieldId, termId) {
const field = this.fields.get(fieldId);
if (!field) return;
- const id = parseInt(termId);
- field.selectedTerms.delete(id);
+ field.selectedTerms.delete(parseInt(termId));
- // Remove from display
- const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
- if (selectedItem) {
- selectedItem.remove();
- }
+ const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
+ if (selectedItem) selectedItem.remove();
- // Update hidden input
- const selectedIds = Array.from(field.selectedTerms);
- field.input.value = selectedIds.join(',');
+ field.input.value = Array.from(field.selectedTerms).join(',');
field.input.dispatchEvent(new Event('change', { bubbles: true }));
}
- /**
- * Add term to field display
- */
- addTermToDisplay(fieldId, id, name, path) {
- const field = this.fields.get(fieldId);
- if (!field || field.selectedContainer.querySelector(`[data-id="${id}"]`)) {
- return; // Already displayed
- }
-
- const item = window.getTemplate('selectedTerm').cloneNode(true);
- item.dataset.id = id;
- item.dataset.path = path;
- item.dataset.name = name;
- item.dataset.taxonomy = field.taxonomy;
- item.querySelector('span').textContent = path;
- item.querySelector('button').title = `Remove ${name}`;
-
- field.selectedContainer.appendChild(item);
- }
-
- /**
- * Update field from hidden input value
- */
updateFieldFromInput(fieldId) {
const field = this.fields.get(fieldId);
if (!field) return;
@@ -874,232 +751,52 @@
field.selectedTerms.clear();
window.removeChildren(field.selectedContainer);
- if (value !== '') {
- const selectedIds = value.split(',')
+ if (value) {
+ value.split(',')
.map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id));
+ .filter(id => !isNaN(id))
+ .forEach(id => field.selectedTerms.add(id));
- selectedIds.forEach(id => field.selectedTerms.add(id));
this.initFieldDisplay(fieldId);
}
}
- /**
- * Handle search input
- */
- handleSearch() {
- const query = this.ui.searchInput.value.trim();
+ /***********************************************************************
+ * NAVIGATION
+ ***********************************************************************/
- if (query.length >= 2 || query.length === 0) {
- // Reset pagination when searching
- this.activeStore.setFilter('page', 1);
- this.activeStore.setFilter('search', query);
-
- window.removeChildren(this.ui.termsList);
-
- 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.');
- }
- }
-
- /**
- * 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);
-
+ this.store.setFilters({ parent: 0, page: 1 });
window.removeChildren(this.ui.termsList);
- this.showLoading();
- this.fetchCurrentTerms();
-
- // Update breadcrumbs
this.ui.breadcrumbs.back.hidden = true;
}
- /**
- * Navigate to child term
- */
navigateToChild(termId, termName) {
- this.activeStore.setFilter('parent', termId);
- this.activeStore.setFilter('page', 1);
-
+ 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;
}
- /**
- * Navigate to specific path level
- */
- navigateToPath(pathLevel) {
- const parentId = parseInt(pathLevel.dataset.id) || 0;
-
- this.activeStore.setFilter('parent', parentId);
- this.activeStore.setFilter('page', 1);
-
+ navigateToPath(parentId) {
+ 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) {
- if (!append) {
- window.removeChildren(this.ui.termsList);
- }
-
- if (terms.length === 0) {
- if (!append) {
- this.showEmptyState();
- }
- return;
- }
-
- // Update breadcrumbs if needed
- const currentParent = this.activeStore.filters.parent || 0;
- this.ui.breadcrumbs.back.hidden = currentParent === 0;
-
- terms.forEach(term => {
- // Check if we have a cached DOM element
- const cachedElement = this.activeStore.getDOMElement(term.id, 'list-item');
-
- 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);
- }
- }
- });
- }
-
- /**
- * Create individual term element
- */
- createTermElement(termData) {
- if (!termData || !termData.name) return null;
-
- const listItem = window.getTemplate('termListItem').cloneNode(true);
- listItem.dataset.id = termData.id;
-
- const isSelected = this.selectedTerms.has(termData.id);
- const checkbox = listItem.querySelector('input');
- const label = listItem.querySelector('label');
- const nameSpan = listItem.querySelector('span, .term-name');
-
- if (checkbox && label && nameSpan) {
- checkbox.id = `${this.currentConfig.container.id}${termData.id}`;
- checkbox.name = `${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`;
- checkbox.value = termData.id;
- checkbox.disabled = !isSelected && this.disabled;
- checkbox.checked = isSelected;
-
- label.htmlFor = checkbox.id;
- label.title = termData.path || termData.name;
- label.dataset.path = termData.path;
-
- nameSpan.textContent = termData.show ? termData.path : termData.name;
- }
-
- if (termData.hasChildren) {
- const childrenToggle = window.getTemplate ?
- window.getTemplate('termChildrenToggle') :
- this.createChildrenToggle();
-
- if (childrenToggle) {
- childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
- listItem.appendChild(childrenToggle);
- }
- }
-
- return listItem;
- }
-
- /**
- * Create children toggle button
- */
- createChildrenToggle() {
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'toggle-children';
- button.innerHTML = '→';
- return button;
- }
-
- /**
- * Update breadcrumb navigation
- */
updateBreadcrumbs(termId, termName) {
- // This is a simplified version - you'd want to maintain a proper breadcrumb trail
- const breadcrumb = window.getTemplate('termBreadcrumb').cloneNode(true);
+ const breadcrumb = window.getTemplate('termBreadcrumb');
breadcrumb.dataset.id = termId;
breadcrumb.textContent = termName;
breadcrumb.title = termName;
- // Remove any existing breadcrumbs after this level
const existingCrumb = this.ui.breadcrumbs.nav.querySelector(`[data-id="${termId}"]`);
if (existingCrumb) {
- // Remove all breadcrumbs after this one
while (existingCrumb.nextElementSibling) {
existingCrumb.nextElementSibling.remove();
}
@@ -1108,21 +805,172 @@
}
}
- /**
- * Show loading state
- */
+ /***********************************************************************
+ * RENDERING
+ ***********************************************************************/
+
+ renderTerms(terms = null, append = false, showPath = false) {
+ if (!terms) terms = this.store.getFiltered();
+
+ if (!append) window.removeChildren(this.ui.termsList);
+
+ if (terms.length === 0) {
+ if (!append) this.showEmptyState();
+ return;
+ }
+
+ 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),
+ name: term.name,
+ hasChildren: term.hasChildren,
+ path: term.path || null,
+ show: showPath
+ });
+
+ if (element) fragment.appendChild(element);
+ });
+
+ this.ui.termsList.appendChild(fragment);
+ }
+
+ createTermElement(termData) {
+ if (!termData?.name) return null;
+
+ const listItem = window.getTemplate('termListItem');
+ listItem.dataset.id = termData.id;
+
+ const isSelected = this.selectedTerms.has(termData.id);
+ const checkbox = listItem.querySelector('input');
+ const label = listItem.querySelector('label');
+ const nameSpan = listItem.querySelector('.term-name');
+
+ checkbox.id = `${this.currentConfig.container.id}${termData.id}`;
+ checkbox.name = `${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`;
+ checkbox.value = termData.id;
+ checkbox.disabled = !isSelected && this.disabled;
+ checkbox.checked = isSelected;
+
+ label.htmlFor = checkbox.id;
+ label.title = termData.path || termData.name;
+ label.dataset.path = termData.path;
+
+ nameSpan.textContent = termData.show ? termData.path : termData.name;
+
+ if (termData.hasChildren) {
+ const childrenToggle = window.getTemplate('termChildrenToggle');
+ childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
+ listItem.appendChild(childrenToggle);
+ }
+
+ return listItem;
+ }
+
+ /***********************************************************************
+ * AUTOCOMPLETE
+ ***********************************************************************/
+
+ showAutocompleteResults(field, terms, query) {
+ if (!field?.autocompleteDropdown) return;
+
+ const dropdown = field.autocompleteDropdown;
+ window.removeChildren(dropdown);
+
+ if (terms.length === 0) {
+ this.showEmptyState('No items found.', dropdown);
+ } else {
+ const fragment = document.createDocumentFragment();
+
+ terms.forEach(term => {
+ const item = this.createAutocompleteItem(field, term);
+ if (item) fragment.appendChild(item);
+ });
+
+ dropdown.appendChild(fragment);
+ }
+
+ // Create button if allowed and no exact match
+ const currentQuery = field.currentAutocompleteQuery || query;
+ if (field.canCreate && currentQuery) {
+ const exactMatch = terms.find(term =>
+ term.name.toLowerCase() === currentQuery.toLowerCase()
+ );
+
+ if (!exactMatch) {
+ dropdown.appendChild(this.createAutocompleteCreateButton(currentQuery));
+ }
+ }
+
+ dropdown.hidden = false;
+ }
+
+ createAutocompleteItem(field, term) {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'autocomplete-item';
+ button.dataset.id = term.id;
+ button.dataset.name = term.name;
+ button.dataset.path = term.path || term.name;
+ button.textContent = term.path || term.name;
+
+ button.addEventListener('click', () => {
+ field.selectedTerms.add(parseInt(term.id));
+ this.addTermDisplay(term.id, term.name, term.path, 'field', field.id);
+
+ field.input.value = Array.from(field.selectedTerms).join(',');
+ field.input.dispatchEvent(new Event('change', { bubbles: true }));
+
+ field.autocompleteDropdown.hidden = true;
+ const input = field.container.querySelector('input[data-autocomplete]');
+ if (input) input.value = '';
+ });
+
+ return button;
+ }
+
+ createAutocompleteCreateButton(query) {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'autocomplete-item create-term';
+ button.dataset.query = query;
+
+ const strong = document.createElement('strong');
+ strong.textContent = 'Create: ';
+
+ button.appendChild(strong);
+ button.appendChild(document.createTextNode(`"${query}"`));
+
+ return button;
+ }
+
+ showAutocompleteError(fieldId) {
+ const field = this.fields.get(fieldId);
+ if (!field?.autocompleteDropdown) return;
+
+ window.removeChildren(field.autocompleteDropdown);
+ this.showEmptyState('Hmmm... something went wrong', field.autocompleteDropdown);
+ }
+
+ /***********************************************************************
+ * UI STATES
+ ***********************************************************************/
+
showLoading() {
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` :
- currentParent === 0 ?
- 'loading items' :
- `loading child items`;
+ const message = searchQuery
+ ? `searching for "${searchQuery}" items`
+ : currentParent === 0
+ ? 'loading items'
+ : 'loading child items';
if (window.typeLoop) {
this.stopTyping = window.typeLoop(this.ui.loading.text, message);
@@ -1131,9 +979,6 @@
}
}
- /**
- * Hide loading state
- */
hideLoading() {
this.ui.loading.loading.hidden = true;
this.modal.classList.remove('loading');
@@ -1143,65 +988,109 @@
}
}
- /**
- * Show empty state message
- */
- showEmptyState(message = 'No items found.') {
- const emptyElement = window.getTemplate('noResults').cloneNode(true);
+ showEmptyState(message = 'No items found.', container = null) {
+ if (!container) container = this.ui.termsList;
- if (message && emptyElement.querySelector('span')) {
- emptyElement.querySelector('span').textContent = message;
+ const emptyElement = window.getTemplate('noResults');
+ const messageSpan = emptyElement.querySelector('span');
+
+ if (message && messageSpan) {
+ messageSpan.textContent = message;
}
- this.ui.termsList.appendChild(emptyElement);
+ container.appendChild(emptyElement);
}
- /**
- * Get field ID from any element within the field
- */
+ /***********************************************************************
+ * UTILITIES
+ ***********************************************************************/
+
getFieldId(element) {
- if (element.dataset.fieldId) {
- return element.dataset.fieldId;
- }
+ if (element.dataset.fieldId) return element.dataset.fieldId;
const fieldContainer = element.closest('[data-field-id]');
- if (fieldContainer) {
- return fieldContainer.dataset.fieldId;
- }
-
- return null;
+ return fieldContainer?.dataset.fieldId || null;
}
- /**
- * Clean up
- */
+ getLabel(taxonomy, type = 'single') {
+ return jvbSettings.labels[taxonomy]?.[type] || taxonomy;
+ }
+
+ async batchFetchTaxonomies() {
+ if (this.taxonomiesToFetch.size === 0) return;
+
+ const taxonomies = Array.from(this.taxonomiesToFetch);
+ this.taxonomiesToFetch.clear();
+
+ this.store.setFilters({
+ taxonomy: taxonomies.join(','),
+ page: 1,
+ search: '',
+ parent: 0
+ });
+ }
+
+ async preloadTaxonomy(taxonomy) {
+ await this.store.setFilters({
+ taxonomy: taxonomy,
+ page: 1,
+ search: '',
+ parent: 0
+ });
+ }
+
+ handleError(error, context, detail = null) {
+ console.error(`Taxonomy ${context} error:`, error, detail);
+
+ if (this.error?.log) {
+ this.error.log(error, {
+ component: 'TaxonomySelector',
+ action: context,
+ detail: detail
+ });
+ }
+
+ if (this.modal?.open) {
+ this.showEmptyState('Error loading. Please try again.');
+ }
+ }
+
+ 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);
+ }
+ });
+ }
+
destroy() {
- // Remove event listeners
document.removeEventListener('click', this.handleClick);
document.removeEventListener('change', this.handleChange);
+ document.removeEventListener('input', this.handleInput);
+ document.removeEventListener('focus', this.handleFocus);
+ document.removeEventListener('blur', this.handleBlur);
- // 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());
-
- // Clear all maps
+ this.store.destroy();
+ this.subscribers.clear();
this.fields.clear();
- this.stores.clear();
- this.storeSubscriptions.clear();
this.selectedTerms.clear();
+ this.searchContexts.clear();
}
}
-/**
- * Initialize singleton
- */
-document.addEventListener('DOMContentLoaded', function() {
- if (!window.jvbSelector) {
- window.jvbSelector = new TaxonomySelector();
- }
+// Initialize on auth ready
+document.addEventListener('DOMContentLoaded', () => {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ window.jvbSelector = new TaxonomySelector();
+ }
+ });
});
--
Gitblit v1.10.0