/**
|
* Centralized Taxonomy Selector
|
* Handles all taxonomy selection fields on the page from a single location
|
*/
|
class TaxonomySelector {
|
constructor() {
|
this.a11y = window.jvbA11y;
|
// this.cache = window.jvbCache;
|
this.error = window.jvbError;
|
this.index = -1;
|
|
this.stores = new Map();
|
|
// Central field management
|
this.fields = new Map(); // fieldId -> field configuration
|
this.terms = new Map(); // taxonomy -> Map(termId -> term data)
|
this.selectedTerms = new Map(); // Current selection (cleared on modal close)
|
|
// Current modal context
|
this.activeField = null;
|
this.currentConfig = null;
|
|
// Modal state
|
this.page = 1;
|
this.hasMore = true;
|
this.isLoading = false;
|
this.searchQuery = '';
|
this.navigationPath = [];
|
this.currentParent = 0;
|
this.currentParentName = '';
|
this.fetchSpecificTerms = false;
|
this.disabled = false;
|
|
// Search debouncing
|
this.searchHandler = null;
|
|
// Prefetch system
|
this.prefetchCache = new Map();
|
this.prefetchQueue = [];
|
this.isPrefetching = false;
|
|
this.init();
|
}
|
|
/**
|
* Initialize the selector
|
*/
|
init() {
|
this.initModal();
|
this.scanExistingFields();
|
this.initGlobalListeners();
|
this.initPrefetching();
|
}
|
|
/**
|
* Scan page for existing taxonomy fields and register them
|
*/
|
scanExistingFields() {
|
const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
|
selectors.forEach(selector => {
|
try {
|
this.registerField(selector);
|
} catch (error) {
|
this.error.log(error, {
|
component: 'TaxonomySelector',
|
action: 'scanExistingFields',
|
container: selector.dataset.name
|
});
|
}
|
});
|
}
|
|
/**
|
* Register a taxonomy field
|
*/
|
registerField(field, options = {}) {
|
let input = field.querySelector('input[type=hidden]');
|
if (!input) {
|
return;
|
}
|
|
if (!('fieldId' in field.dataset)) {
|
field.dataset.fieldId = this.createFieldId(field);
|
}
|
let fieldId = field.dataset.fieldId;
|
let button = field.querySelector('button.taxonomy-toggle');
|
|
let config = {
|
id: fieldId,
|
input: input,
|
container: field,
|
taxonomy: button.dataset.taxonomy,
|
name: field.dataset.field,
|
singular: button.dataset.singular,
|
plural: button.dataset.plural,
|
maxSelection: parseInt(button.dataset.max) || 0,
|
canSearch: 'search' in button.dataset,
|
canCreate: 'creatable' in button.dataset,
|
isRequired: 'required' in button.dataset,
|
selectedTerms: new Set(input.value.split(',')),
|
toggle: button,
|
selectedContainer: field.querySelector('.selected-items'),
|
...options
|
};
|
|
this.fields.set(fieldId, config);
|
|
// Initialize display for any pre-selected values
|
this.initFieldDisplay(fieldId);
|
|
return fieldId;
|
}
|
|
/**
|
* Create unique field ID
|
*/
|
createFieldId(field) {
|
let input = field.querySelector('input[type=hidden]');
|
this.index++;
|
return 'selector-'+this.index;
|
|
}
|
|
/**
|
* Initialize display for a field with existing values
|
*/
|
initFieldDisplay(fieldId) {
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
const value = field.input.value.trim();
|
if (value !== '') {
|
const selectedIds = value.split(',')
|
.map(id => parseInt(id.trim()))
|
.filter(id => !isNaN(id));
|
|
if (selectedIds.length > 0) {
|
this.updateFieldDisplay(fieldId, selectedIds);
|
}
|
}
|
}
|
|
/**
|
* Update field display with selected term IDs
|
*/
|
async updateFieldDisplay(fieldId, selectedIds) {
|
const field = this.fields.get(fieldId);
|
if (!field || selectedIds.length === 0) return;
|
|
// Check if we already have these terms cached
|
const taxonomy = field.taxonomy;
|
const needsFetch = [];
|
const cachedTerms = [];
|
|
selectedIds.forEach(termId => {
|
if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) {
|
const term = this.terms.get(taxonomy).get(termId);
|
cachedTerms.push(term);
|
field.selectedTerms.add(termId);
|
} else {
|
needsFetch.push(termId);
|
}
|
});
|
|
// 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 {
|
this.fetchSpecificTerms = needsFetch.join(',');
|
const fetchedTerms = await this.fetchTerms(fieldId);
|
|
fetchedTerms.forEach(term => {
|
field.selectedTerms.add(term.id);
|
this.addTermToDisplay(fieldId, term.id, term.name, term.path);
|
});
|
} catch (error) {
|
console.error('Failed to fetch missing terms:', error);
|
} finally {
|
this.fetchSpecificTerms = false;
|
}
|
}
|
}
|
|
/**
|
* Initialize modal elements
|
*/
|
initModal() {
|
this.modalID = 'dialog#jvb-selector';
|
this.modal = document.querySelector(this.modalID);
|
|
if (!this.modal) {
|
console.warn('Taxonomy selector modal not found');
|
return;
|
}
|
|
this.initModalElements();
|
|
// Initialize modal instance
|
this.modalInstance = new window.jvbModal(this.modal, {
|
handleForm: false,
|
save: null,
|
open: null,
|
onOpen: () => this.openModal(),
|
onClose: () => this.closeModal()
|
});
|
}
|
|
/**
|
* Initialize modal element references
|
*/
|
initModalElements() {
|
this.elements = {
|
searchInput: this.modal.querySelector('input[type=search]'),
|
termsList: this.modal.querySelector('.items-container'),
|
termsWrap: this.modal.querySelector('.items-wrap'),
|
breadcrumbs: this.modal.querySelector('nav.term-navigation'),
|
loading: this.modal.querySelector('.loading'),
|
loadingText: this.modal.querySelector('.loading span'),
|
clearSearch: this.modal.querySelector('.clear-search'),
|
selectedTerms: this.modal.querySelector('.selected-items'),
|
backButton: this.modal.querySelector('.back-to-parent'),
|
sentinel: this.modal.querySelector('.scroll-sentinel'),
|
modalTitle: this.modal.querySelector('#modal-title'),
|
modalContent: this.modal.querySelector('.modal-content'),
|
createNewSection: this.modal.querySelector('.create-new-term'),
|
favouriteTerms: this.modal.querySelector('.favourite-terms'),
|
};
|
|
// Initialize intersection observer for infinite scroll
|
this.observer = new IntersectionObserver((entries) => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
|
this.fetchTerms(this.activeField);
|
}
|
});
|
}, {
|
root: this.elements.termsWrap,
|
threshold: 0.5
|
});
|
}
|
|
/**
|
* Set up global event delegation
|
*/
|
initGlobalListeners() {
|
document.addEventListener('click', this.handleClick.bind(this));
|
document.addEventListener('change', this.handleChange.bind(this));
|
}
|
|
/**
|
* Handle global click events
|
*/
|
handleClick(e) {
|
// Handle taxonomy toggle buttons
|
const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
|
if (toggleButton) {
|
e.preventDefault();
|
this.handleToggleClick(toggleButton);
|
return;
|
}
|
|
// Handle remove selected term buttons
|
const removeButton = window.targetCheck(e, 'button.remove-item');
|
if (removeButton && e.target.closest('.jvb-selector')) {
|
const fieldId = this.getFieldId(removeButton);
|
const termId = removeButton.closest('.selected-item').dataset.id;
|
this.removeSelectedTerm(fieldId, termId);
|
return;
|
}
|
|
// Handle modal close button
|
if (e.target.matches('.modal-close')) {
|
if (this.modalInstance) {
|
this.modalInstance.handleClose();
|
}
|
return;
|
}
|
|
// Handle clicks within the modal
|
if (this.modal && this.modal.contains(e.target)) {
|
this.handleModalClick(e);
|
}
|
}
|
|
/**
|
* Handle global change events
|
*/
|
handleChange(e) {
|
// Handle hidden input changes for taxonomy fields
|
const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
|
if (taxonomyField && e.target.type === 'hidden') {
|
const fieldId = this.getFieldId(e.target);
|
this.updateFieldFromInput(fieldId);
|
return;
|
}
|
|
// Handle modal changes
|
if (this.modal && this.modal.contains(e.target)) {
|
this.handleModalChange(e);
|
}
|
}
|
|
/**
|
* Handle toggle button click
|
*/
|
handleToggleClick(toggle) {
|
try {
|
const fieldId = this.getFieldId(toggle);
|
const field = this.fields.get(fieldId);
|
|
if (!field) {
|
console.error('Field not found for toggle:', fieldId);
|
return;
|
}
|
|
this.setActiveField(fieldId);
|
this.modalInstance.handleOpen();
|
|
} catch (error) {
|
console.error('Error handling toggle click:', error);
|
this.error?.handleError(error, {
|
component: 'TaxonomySelector',
|
action: 'handleToggleClick'
|
});
|
}
|
}
|
|
/**
|
* Set the active field for modal operations
|
*/
|
setActiveField(fieldId) {
|
this.activeField = fieldId;
|
this.currentConfig = this.fields.get(fieldId);
|
|
// Clear modal selection state
|
this.selectedTerms.clear();
|
|
// Copy field's current selections to modal state
|
if (this.currentConfig.selectedTerms) {
|
this.currentConfig.selectedTerms.forEach(termId => {
|
// Find term data to populate modal selection
|
const taxonomy = this.currentConfig.taxonomy;
|
if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) {
|
const term = this.terms.get(taxonomy).get(termId);
|
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');
|
if (selectedItem) {
|
this.removeSelectedTermFromModal(selectedItem.dataset.id);
|
}
|
} else if (window.targetCheck(e, '.back-to-parent')) {
|
this.toParent();
|
} else if (window.targetCheck(e, '.toggle-children')) {
|
let termItem = e.target.closest('li');
|
this.toChild(
|
parseInt(termItem.dataset.id),
|
termItem.querySelector('.term-name').textContent
|
);
|
} else if (window.targetCheck(e, '.path-level')) {
|
let pathLevel = window.targetCheck(e, '.path-level');
|
if (pathLevel.textContent !== this.currentParentName) {
|
this.navigateToPath(pathLevel);
|
}
|
}
|
}
|
|
/**
|
* Handle changes within modal (checkboxes)
|
*/
|
handleModalChange(e) {
|
if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
|
e.preventDefault();
|
e.stopPropagation();
|
|
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 and initialize
|
*/
|
openModal() {
|
if (!this.activeField || !this.currentConfig) {
|
console.error('No active field set for modal');
|
return;
|
}
|
|
this.resetModalState();
|
this.updateModalForTaxonomy();
|
this.observer.observe(this.elements.sentinel);
|
|
// Set up search if enabled
|
if (this.currentConfig.canSearch) {
|
this.elements.searchInput.focus();
|
this.searchHandler = window.debounce(() => this.handleSearch(), 300);
|
this.elements.searchInput.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.updateModalSelections();
|
|
// Fetch terms
|
this.fetchTerms(this.activeField);
|
}
|
|
/**
|
* Close modal and save selections
|
*/
|
closeModal() {
|
this.observer.unobserve(this.elements.sentinel);
|
window.removeChildren(this.elements.termsList);
|
|
// Save selections to active field
|
if (this.activeField) {
|
this.saveSelectionsToField(this.activeField);
|
}
|
|
// Cleanup
|
if (this.currentConfig?.canSearch && this.searchHandler) {
|
this.elements.searchInput.removeEventListener('input', this.searchHandler);
|
}
|
|
if (this.creator) {
|
delete this.creator;
|
}
|
|
this.activeField = null;
|
this.currentConfig = null;
|
}
|
|
/**
|
* Reset modal state
|
*/
|
resetModalState() {
|
this.page = 1;
|
this.hasMore = true;
|
this.isLoading = false;
|
this.searchQuery = '';
|
this.navigationPath = [];
|
this.currentParent = 0;
|
this.currentParentName = '';
|
this.disabled = false;
|
|
window.removeChildren(this.elements.termsList);
|
window.removeChildren(this.elements.selectedTerms);
|
this.elements.searchInput.value = '';
|
}
|
|
/**
|
* Update modal content for current taxonomy
|
*/
|
updateModalForTaxonomy() {
|
if (!this.currentConfig) return;
|
|
this.elements.modalTitle.textContent = `Select ${this.currentConfig.plural}`;
|
|
const searchWrapper = this.modal.querySelector('.search-wrapper');
|
if (searchWrapper) {
|
searchWrapper.style.display = this.currentConfig.canSearch ? 'block' : 'none';
|
}
|
|
if (this.elements.createNewSection) {
|
this.elements.createNewSection.style.display = this.currentConfig.canCreate ? 'block' : 'none';
|
this.elements.createNewSection.hidden = !this.currentConfig.canCreate;
|
}
|
|
const openMessage = `Opened ${this.currentConfig.singular} selection. Choose from checkboxes or search to filter results.`;
|
this.a11y?.announce(openMessage);
|
}
|
|
/**
|
* Update modal selections display
|
*/
|
updateModalSelections() {
|
window.removeChildren(this.elements.selectedTerms);
|
|
this.selectedTerms.forEach((termData, id) => {
|
this.addTermToModalDisplay(id, termData.name, termData.path);
|
});
|
|
this.checkSelectionLimits();
|
}
|
|
/**
|
* Add selected term to modal
|
*/
|
addSelectedTermToModal(id, name, path) {
|
this.selectedTerms.set(id, {
|
id: id,
|
name: name,
|
path: path
|
});
|
|
this.addTermToModalDisplay(id, name, path);
|
this.checkSelectionLimits();
|
|
// Check the corresponding checkbox
|
const checkbox = this.elements.termsList.querySelector(`input[value="${id}"]`);
|
if (checkbox) {
|
checkbox.checked = true;
|
}
|
}
|
|
/**
|
* Remove selected term from modal
|
*/
|
removeSelectedTermFromModal(id) {
|
this.selectedTerms.delete(id);
|
|
// Remove from modal display
|
const selectedItem = this.elements.selectedTerms.querySelector(`[data-id="${id}"]`);
|
if (selectedItem) {
|
selectedItem.remove();
|
}
|
|
// Uncheck the corresponding checkbox
|
const checkbox = this.elements.termsList.querySelector(`input[value="${id}"]`);
|
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.elements.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.elements.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
if (!checkbox.checked) {
|
checkbox.disabled = 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);
|
});
|
|
// Update hidden input
|
const selectedIds = Array.from(field.selectedTerms);
|
field.input.value = selectedIds.join(',');
|
field.input.dispatchEvent(new Event('change', { bubbles: true }));
|
}
|
|
/**
|
* Remove selected term from field
|
*/
|
removeSelectedTerm(fieldId, termId) {
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
const id = parseInt(termId);
|
field.selectedTerms.delete(id);
|
|
// Remove from display
|
const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
|
if (selectedItem) {
|
selectedItem.remove();
|
}
|
|
// Update hidden input
|
const selectedIds = Array.from(field.selectedTerms);
|
field.input.value = selectedIds.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;
|
|
const value = field.input.value.trim();
|
field.selectedTerms.clear();
|
window.removeChildren(field.selectedContainer);
|
|
if (value !== '') {
|
const selectedIds = value.split(',')
|
.map(id => parseInt(id.trim()))
|
.filter(id => !isNaN(id));
|
|
this.updateFieldDisplay(fieldId, selectedIds);
|
}
|
}
|
|
/**
|
* Handle search input
|
*/
|
handleSearch() {
|
let query = this.elements.searchInput.value.trim();
|
|
if (query !== this.searchQuery) {
|
this.searchQuery = query;
|
this.page = 1;
|
this.hasMore = true;
|
window.removeChildren(this.elements.termsList);
|
|
if (query.length >= 2 || query.length === 0) {
|
this.fetchTerms(this.activeField, false, true);
|
} else {
|
this.hideLoading();
|
this.showEmptyState('No Results. \nEnter at least 2 characters to search.');
|
}
|
}
|
}
|
|
/**
|
* Navigate to parent term
|
*/
|
toParent() {
|
this.navigationPath.pop();
|
let parent = this.navigationPath[this.navigationPath.length - 1];
|
this.currentParent = parent ? parent.id : 0;
|
this.currentParentName = parent ? parent.name : '';
|
window.removeChildren(this.elements.termsList);
|
this.page = 1;
|
this.hasMore = true;
|
this.fetchTerms(this.activeField);
|
}
|
|
/**
|
* Navigate to child term
|
*/
|
toChild(id, name) {
|
this.navigationPath.push({ id: id, name: name });
|
this.currentParent = id;
|
this.currentParentName = name;
|
window.removeChildren(this.elements.termsList);
|
this.page = 1;
|
this.hasMore = true;
|
this.fetchTerms(this.activeField);
|
}
|
|
/**
|
* Navigate to specific path level
|
*/
|
navigateToPath(pathLevel) {
|
window.removeChildren(this.elements.termsList);
|
let level = parseInt(pathLevel.dataset.level);
|
this.navigationPath = this.navigationPath.slice(0, level + 1);
|
this.currentParent = parseInt(pathLevel.dataset.id);
|
this.currentParentName = pathLevel.textContent;
|
this.page = 1;
|
this.hasMore = true;
|
this.fetchTerms(this.activeField);
|
}
|
|
/**
|
* Fetch terms for the active field
|
*/
|
async fetchTerms(fieldId, forceRefresh = false, isSearch = false) {
|
|
if (this.isLoading || !fieldId) return;
|
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
if (!this.stores.has(field.taxonomy)) {
|
this.stores.set(field.taxonomy, new window.jvbStore({
|
name: field.taxonomy,
|
endpoint: 'terms',
|
filters: {
|
taxonomy: field.taxonomy,
|
page: 1,
|
search: '',
|
parent: 0
|
}
|
}));
|
}
|
|
let store = this.stores.get(field.taxonomy);
|
store.fetch();
|
|
return;
|
|
try {
|
this.showLoading();
|
|
const requestUrl = `${jvbSettings.api}terms?${this.buildRequest(field.taxonomy)}`;
|
|
const response = await this.cache.fetchWithCache(requestUrl, {
|
method: 'GET',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': jvbSettings.nonce
|
}
|
}, {
|
content: field.taxonomy,
|
forceRefresh: forceRefresh
|
});
|
|
// Handle specific term fetching
|
if (this.fetchSpecificTerms) {
|
this.fetchSpecificTerms = false;
|
return response.terms || [];
|
}
|
|
if (response && response.terms && response.terms.length !== 0) {
|
this.hasMore = response.pagination?.has_more || false;
|
this.renderTerms(response.terms, this.page > 1, isSearch);
|
|
if (this.hasMore) {
|
this.nextPage();
|
}
|
} else {
|
if (this.page === 1) {
|
this.showEmptyState();
|
}
|
this.hasMore = false;
|
}
|
|
return response.terms || [];
|
} catch (error) {
|
this.handleError(error);
|
return [];
|
} finally {
|
this.hideLoading();
|
}
|
}
|
|
/**
|
* Build request parameters
|
*/
|
buildRequest(taxonomy = null) {
|
let params = new URLSearchParams({
|
taxonomy: taxonomy || this.currentConfig?.taxonomy || '',
|
parent: this.currentParent,
|
search: this.searchQuery,
|
page: this.page
|
});
|
|
if (this.fetchSpecificTerms) {
|
params.append('termIDs', this.fetchSpecificTerms);
|
}
|
|
return params.toString();
|
}
|
|
/**
|
* Render terms list
|
*/
|
renderTerms(terms, append = false, showPath = false) {
|
|
|
if (!append) {
|
window.removeChildren(this.elements.termsList);
|
}
|
|
if (terms.length === 0) {
|
if (!append) {
|
this.showEmptyState();
|
}
|
this.a11y?.announce(0, append);
|
return;
|
}
|
|
this.updateBreadcrumbs();
|
|
for (const [index, term] of Object.entries(terms)) {
|
if (!term) continue;
|
|
// Store term in global cache
|
const taxonomy = this.currentConfig.taxonomy;
|
if (!this.terms.has(taxonomy)) {
|
this.terms.set(taxonomy, new Map());
|
}
|
this.terms.get(taxonomy).set(term.id, term);
|
|
this.createTermElement({
|
id: parseInt(term.id),
|
name: term.name,
|
hasChildren: term.hasChildren,
|
path: term.path || null,
|
show: showPath
|
});
|
}
|
|
this.a11y?.announce(terms.length, append);
|
}
|
|
/**
|
* Create individual term element
|
*/
|
createTermElement(termData) {
|
if (!termData || !termData.name) return;
|
|
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('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);
|
}
|
}
|
|
this.elements.termsList.appendChild(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() {
|
window.removeChildren(this.elements.breadcrumbs);
|
this.elements.breadcrumbs.appendChild(this.elements.backButton);
|
this.elements.backButton.hidden = this.currentParent === 0;
|
|
this.navigationPath.forEach((pathItem, index) => {
|
const breadcrumb = window.getTemplate ?
|
window.getTemplate('termBreadcrumb') :
|
this.createBreadcrumbElement();
|
|
breadcrumb.dataset.level = index;
|
breadcrumb.dataset.id = pathItem.id;
|
breadcrumb.title = pathItem.path || pathItem.name;
|
breadcrumb.textContent = pathItem.name;
|
|
this.elements.breadcrumbs.appendChild(breadcrumb);
|
});
|
}
|
|
/**
|
* Create breadcrumb element
|
*/
|
createBreadcrumbElement() {
|
const button = document.createElement('button');
|
button.type = 'button';
|
button.className = 'path-level';
|
return button;
|
}
|
|
/**
|
* Show loading state
|
*/
|
showLoading() {
|
this.isLoading = true;
|
this.elements.loading.hidden = false;
|
this.modal.classList.add('loading');
|
|
let message = this.searchQuery !== '' ?
|
`searching for "${this.searchQuery}" items` :
|
this.currentParentName === '' ?
|
'loading items' :
|
`loading ${this.currentParentName} items`;
|
|
if (window.typeLoop) {
|
this.stopTyping = window.typeLoop(this.elements.loadingText, message);
|
} else {
|
this.elements.loadingText.textContent = message;
|
}
|
}
|
|
/**
|
* Hide loading state
|
*/
|
hideLoading() {
|
this.isLoading = false;
|
this.elements.loading.hidden = true;
|
this.modal.classList.remove('loading');
|
|
if (this.stopTyping) {
|
this.stopTyping();
|
}
|
}
|
|
/**
|
* Show empty state message
|
*/
|
showEmptyState(message = '') {
|
const emptyElement = window.getTemplate('noResults');
|
|
if (message && emptyElement.querySelector('span')) {
|
emptyElement.querySelector('span').textContent = message;
|
}
|
|
this.elements.termsList.appendChild(emptyElement);
|
}
|
|
/**
|
* Move to next page
|
*/
|
nextPage() {
|
if (this.hasMore) {
|
this.page++;
|
}
|
}
|
|
/**
|
* Handle API errors
|
*/
|
handleError(error) {
|
console.error('Taxonomy selector error:', error);
|
|
if (this.error && this.error.log) {
|
return this.error.log(error, {
|
component: 'TaxonomySelector',
|
action: 'fetchTerms'
|
}, () => this.fetchTerms(this.activeField));
|
} else {
|
this.showEmptyState('Error loading terms. Please try again.');
|
}
|
}
|
|
/**
|
* Initialize prefetching system
|
*/
|
initPrefetching() {
|
if (document.readyState === 'complete') {
|
this.startPrefetching();
|
} else {
|
window.addEventListener('load', () => {
|
this.scheduleIdlePrefetch();
|
});
|
}
|
}
|
|
/**
|
* Schedule prefetching during browser idle time
|
*/
|
scheduleIdlePrefetch() {
|
if ('requestIdleCallback' in window) {
|
requestIdleCallback((deadline) => {
|
this.startPrefetching(deadline);
|
}, { timeout: 5000 });
|
} else {
|
setTimeout(() => this.startPrefetching(), 1000);
|
}
|
}
|
|
/**
|
* Start the prefetching process
|
*/
|
startPrefetching() {
|
const visibleTaxonomies = new Set();
|
document.querySelectorAll('button.taxonomy-toggle').forEach(button => {
|
visibleTaxonomies.add(button.dataset.taxonomy);
|
});
|
|
visibleTaxonomies.forEach(taxonomy => {
|
this.prefetchTaxonomyTerms(taxonomy);
|
});
|
}
|
|
/**
|
* Prefetch terms for a specific taxonomy
|
*/
|
async prefetchTaxonomyTerms(taxonomy) {
|
try {
|
const response = await this.cache.fetchWithCache(
|
`${jvbSettings.api}terms?taxonomy=${taxonomy}&page=1`,
|
{
|
method: 'GET',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': jvbSettings.nonce
|
}
|
},
|
{ content: taxonomy }
|
);
|
|
if (response.terms) {
|
if (!this.terms.has(taxonomy)) {
|
this.terms.set(taxonomy, new Map());
|
}
|
response.terms.forEach(term => {
|
this.terms.get(taxonomy).set(term.id, term);
|
});
|
}
|
} catch (error) {
|
console.warn(`Failed to prefetch terms for ${taxonomy}:`, error);
|
}
|
}
|
|
/**
|
* Get field ID from any element within the field
|
*/
|
getFieldId(element) {
|
// Try multiple approaches to find the field ID
|
if (element.dataset.fieldId) {
|
return element.dataset.fieldId;
|
}
|
|
const fieldContainer = element.closest('[data-field-id]');
|
if (fieldContainer) {
|
return fieldContainer.dataset.fieldId ||
|
fieldContainer.dataset.field ||
|
fieldContainer.dataset.name;
|
}
|
|
return null;
|
}
|
|
/**
|
* Utility: delay for prefetching
|
*/
|
delay(ms) {
|
return new Promise(resolve => setTimeout(resolve, ms));
|
}
|
|
/**
|
* Clean up
|
*/
|
destroy() {
|
// Remove event listeners
|
document.removeEventListener('click', this.handleClick);
|
document.removeEventListener('change', this.handleChange);
|
|
// Clear intervals and cleanup
|
this.observer?.disconnect();
|
|
// Clear all maps
|
this.fields.clear();
|
this.terms.clear();
|
this.selectedTerms.clear();
|
}
|
}
|
|
/**
|
* Initialize singleton
|
*/
|
document.addEventListener('DOMContentLoaded', function() {
|
if (!window.jvbSelector) {
|
window.jvbSelector = new TaxonomySelector();
|
}
|
});
|