class TaxonomySelector {
|
constructor() {
|
this.container = document.querySelector('dialog#jvb-selector');
|
if (!this.container) return;
|
|
this.a11y = window.jvbA11y;
|
this.error = window.jvbError;
|
|
this.subscribers = new Set();
|
this.fields = new Map();
|
this.selectedTerms = new Map(); // a map of fieldId => Set of selected term Ids
|
this.loadedTaxonomies = new Set(); // a set of taxonomies, to know whether we should preload a newly registered field
|
this.batchFetch = new Set();
|
|
this.activeField = null;
|
this.isInitializing = true;
|
this.init();
|
}
|
|
init() {
|
this.initStore();
|
this.initElements();
|
this.initModal();
|
this.scanExistingFields();
|
this.initListeners();
|
|
if (this.needsCreator() && window.jvbTaxCreator) {
|
this.creator = new window.jvbTaxCreator(this);
|
}
|
this.isInitializing = false
|
this.batchFetchTaxonomies().then(()=> {});
|
}
|
|
initStore() {
|
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;
|
|
this.store.subscribe(this.handleStoreEvent.bind(this));
|
}
|
|
/******************************************************************
|
ELEMENTS
|
******************************************************************/
|
initElements() {
|
this.selectors = {
|
search: {
|
input: '[type=search]',
|
clear: '.clear-search',
|
container: '.search-wrapper',
|
results: '.search-results'
|
},
|
terms: {
|
list: '.items-container',
|
wrap: '.items-wrap',
|
sentinel: '.scroll-sentinel',
|
},
|
nav: {
|
nav: 'nav.term-navigation',
|
back: '.back-to-parent',
|
child: '.toggle-children',
|
pathLevel: '.path-level',
|
},
|
loading: {
|
loading: '.loading',
|
text: '.loading span',
|
},
|
selected: '.selected-items',
|
modal: {
|
title: '#modal-title',
|
content: '.modal-content',
|
count: '.selection-count'
|
},
|
favourites: '.favourite-terms',
|
field: {
|
toggle: 'button.taxonomy-toggle',
|
value: 'input[type="hidden"]',
|
selected: '.selected-items',
|
dropdown: '.search-results',
|
search: '[data-autocomplete]',
|
}
|
}
|
|
this.ui = window.uiFromSelectors(this.selectors);
|
}
|
|
initListeners() {
|
this.observer = new IntersectionObserver((entries) => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting) {
|
this.nextPage();
|
}
|
});
|
}, {
|
root: this.ui.terms.sentinel,
|
threshold: 0.5
|
});
|
|
this.clickHandler = this.handleClick.bind(this);
|
this.changeHandler = this.handleChange.bind(this);
|
this.inputHandler = this.handleInput.bind(this);
|
this.focusHandler = this.handleFocus.bind(this);
|
this.blurHandler = this.handleBlur.bind(this);
|
|
document.addEventListener('click', this.clickHandler);
|
document.addEventListener('change', this.changeHandler);
|
document.addEventListener('input', this.inputHandler);
|
document.addEventListener('focus', this.focusHandler, true);
|
document.addEventListener('blur', this.blurHandler, true);
|
}
|
|
handleClick(e) {
|
const fieldId = this.getFieldId(e.target);
|
const field = this.fields.get(fieldId);
|
if (!fieldId || !field) return;
|
|
const autoComplete = window.targetCheck(e, '[data-autocomplete-select]');
|
if (autoComplete) {
|
let termId = parseInt(autoComplete.dataset.id);
|
this.addSelected(termId, fieldId);
|
if (field.ui.dropdown) {
|
field.ui.dropdown.hidden = true;
|
}
|
|
if (field.ui.search) {
|
field.ui.search.value = '';
|
}
|
}
|
|
const toggleButton = window.targetCheck(e, field.ui.toggle);
|
if (toggleButton) {
|
e.preventDefault();
|
this.openModal(fieldId);
|
return;
|
}
|
|
const removeButton = window.targetCheck(e, 'button.remove-item');
|
if (removeButton) {
|
const fieldId = this.getFieldId(removeButton);
|
const termId = removeButton.closest('.selected-item').dataset.id??false;
|
if (fieldId && termId) {
|
this.removeSelected(termId, fieldId);
|
}
|
return;
|
}
|
|
if (e.target.matches('.modal-close')) {
|
this.modal?.handleClose();
|
return;
|
}
|
|
const backToParent = window.targetCheck(e, this.selectors.nav.back);
|
if (backToParent) {
|
this.navigateToParent();
|
return;
|
}
|
|
const toChild = window.targetCheck(e, this.selectors.nav.child);
|
if (toChild) {
|
const termItem = e.target.closest('li');
|
const termId = parseInt(termItem.dataset.id);
|
|
if (termId) {
|
this.navigateTo(termId);
|
}
|
return;
|
}
|
|
const pathLevel = window.targetCheck(e, this.selectors.nav.pathLevel);
|
if (pathLevel) {
|
const termId = parseInt(pathLevel.dataset.id)??0;
|
this.navigateTo(termId);
|
}
|
|
const dropdown = window.targetCheck(e, field.selectors.dropdown);
|
if (dropdown) {
|
// reset the timer for hiding the dropdown
|
this.scheduleHideDropdown(fieldId);
|
return;
|
}
|
|
const clearSearch = window.targetCheck(e, this.selectors.search.clear);
|
if (clearSearch) {
|
const field = this.currentField();
|
if (field && field.ui.search) {
|
field.ui.search.value = '';
|
this.store.setFilters({
|
search: '',
|
page: 1,
|
parent: this.store.filters.parent || 0
|
});
|
}
|
if (this.ui.search.input) {
|
this.ui.search.input.value = '';
|
}
|
}
|
|
}
|
handleChange(e) {
|
if (!this.container.contains(e.target)) {
|
return;
|
}
|
if (e.target.type !== 'checkbox') return;
|
e.preventDefault();
|
e.stopPropagation();
|
|
const termId = parseInt(e.target.dataset.id);
|
let fieldId = this.getFieldId(e.target);
|
if (e.target.checked) {
|
this.addSelected(termId, fieldId);
|
} else {
|
this.removeSelected(termId, fieldId);
|
}
|
}
|
//For search in modal or field autocomplete
|
handleInput(e) {
|
let fieldId = this.getFieldId(e.target)??this.activeField;
|
if (!fieldId) return;
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
if (!this.container.open) {
|
this.activeField = fieldId;
|
}
|
|
const query = e.target.value.trim();
|
window.debouncer.schedule(
|
`${fieldId}-search`,
|
async () => {
|
await this.store.setFilters({
|
taxonomy: field.taxonomy,
|
search: query,
|
page: 1,
|
parent: query ? 0 : (this.store.filters.parent || 0)
|
});
|
if (this.container.open) {
|
window.removeChildren(this.ui.terms.list);
|
}
|
},
|
100
|
);
|
}
|
|
handleFocus(e) {
|
const fieldId = this.getFieldId(e.target);
|
const field = this.fields.get(fieldId);
|
if (!fieldId || !field) return;
|
if (!field.hasAutocomplete && !field.hasSearch) return;
|
|
window.debouncer.cancel(`${fieldId}-search-results`);
|
|
if (!this.container.open){
|
this.activeField = fieldId;
|
this.preloadTaxonomy(field.taxonomy);
|
}
|
}
|
|
//Hide autocomplete dropdown on blur
|
handleBlur(e) {
|
const fieldId = this.getFieldId(e.target);
|
const field = this.fields.get(fieldId);
|
if (!fieldId || ! field) return;
|
if (!field.hasAutocomplete) return;
|
|
this.scheduleHideDropdown(fieldId);
|
}
|
|
scheduleHideDropdown(fieldId){
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
window.debouncer.schedule(
|
`${fieldId}-search-results`,
|
() => {
|
this.activeField = null;
|
field.ui.dropdown.hidden = true;
|
},
|
1500
|
);
|
}
|
|
/******************************************************************
|
MODAL
|
******************************************************************/
|
initModal() {
|
this.modalID = 'dialog#jvb-selector';
|
this.container = document.querySelector(this.modalID);
|
|
this.modal = new window.jvbModal(
|
this.container,
|
{
|
handleForm: false,
|
save: null,
|
open: null
|
}
|
);
|
this.modal.subscribe((event, data) => {
|
switch (event) {
|
|
}
|
});
|
}
|
|
toggleModal(fieldId, open = true) {
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
if (open) {
|
this.openModal(fieldId);
|
} else {
|
this.closeModal();
|
}
|
}
|
|
openModal(fieldId) {
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
|
this.activeField = fieldId;
|
this.ui.modal.title.textContent = `Select ${field.plural}`;
|
if (this.ui.search.container) {
|
this.ui.search.container.hidden = !field.canSearch;
|
}
|
if (this.ui.create.details) {
|
this.ui.create.details.hidden = !field.canCreate;
|
|
if (this.ui.create.summary) {
|
this.ui.create.summary.textContent = `Add new ${field.singular}`;
|
}
|
if (this.ui.create.label.name) {
|
this.ui.create.label.name.textContent = `Name this ${field.singular}`;
|
}
|
if (this.ui.create.label.parent) {
|
this.ui.create.label.parent.textContent = `Nest it under`;
|
}
|
}
|
let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
|
|
window.removeChildren(this.ui.terms.list);
|
this.modal.handleOpen();
|
this.setLoading();
|
|
this.store.setFilters({
|
taxonomy: field.taxonomy,
|
page: 1,
|
search: '',
|
parent: 0,
|
});
|
|
this.a11y.announce(message);
|
}
|
|
closeModal() {
|
this.modal.handleClose();
|
const field = this.fields.get(this.activeField);
|
if (!field) return;
|
this.observer.unobserve(this.ui.terms.sentinel);
|
window.removeChildren(this.ui.terms.list);
|
|
this.notify('selected-terms', {
|
terms: this.selectedTerms.get(this.activeField),
|
taxonomy: field.taxonomy
|
});
|
|
this.activeField = null;
|
|
let message = `Closed ${field.singular} selector.`;
|
this.a11y.announce(message);
|
}
|
|
navigateToParent() {
|
const current = this.store.filters.parent;
|
if (current === 0) return;
|
let term = this.store.get(parseInt(current));
|
if (!term) return;
|
let parent = term.parent;
|
this.navigateTo(parseInt(parent));
|
}
|
navigateTo(termId = 0) {
|
termId = parseInt(termId)??0;
|
this.store.setFilters({parent: termId, page: 1});
|
window.removeChildren(this.ui.terms.list);
|
this.updateBreadcrumbs(termId);
|
}
|
|
nextPage() {
|
let current = this.store.filters.page;
|
let page = Math.min(current++, this.store.lastResponse.total);
|
this.store.setFilters({page:page});
|
}
|
prevPage() {
|
let current = this.store.filters.page;
|
let page = Math.max(current - 1, 1);
|
this.store.setFilters({page:page});
|
}
|
|
addTermToModal(termId) {
|
const term = this.store.get(termId);
|
if (!term) return;
|
|
const item = window.getTemplate('selectedTerm');
|
item.dataset.id = termId;
|
item.querySelector('span').textContent = term.path;
|
item.querySelector('button').title = `Remove ${name}`;
|
|
this.ui.selected.append(item);
|
}
|
/******************************************************************
|
FIELDS
|
******************************************************************/
|
scanExistingFields(container = document.body) {
|
container.querySelectorAll('[data-type="selector"]').forEach(
|
selector => {
|
try {
|
this.registerField(selector);
|
} catch (error) {
|
this.error.log(error, {
|
component: 'TaxonomySelector',
|
action: 'scanExistingFields',
|
container: selector.dataset.name
|
});
|
}
|
}
|
);
|
}
|
|
registerField(element, options = {}) {
|
let input = element.querySelector('input[type="hidden"]');
|
if (!input) {
|
console.warn('TaxonomySelector: No hidden input found for field', element);
|
return;
|
}
|
|
if (!('fieldId' in element.dataset)) {
|
element.dataset.fieldId = window.generateID('selector');
|
}
|
const fieldId = element.dataset.fieldId;
|
|
|
let selectors = this.selectors.field;
|
let button = element.querySelector('button.taxonomy-toggle');
|
if (options.size === 0){
|
if (!button) return;
|
options = button.dataset;
|
if (options.size === 0) return;
|
} else if (Object.hasOwn(options, 'toggle')) {
|
button = document.querySelector(options.toggle);
|
selectors.toggle = options.toggle;
|
}
|
|
const config = {
|
id: fieldId,
|
value: input,
|
element: element,
|
taxonomy: options.taxonomy??false,
|
singular: options.single??'',
|
plural: options.plural??'',
|
name: element.dataset.field,
|
canSearch: Object.hasOwn(options, 'search'),
|
limit: options.limit??0,
|
hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
|
canCreate: Object.hasOwn(options, 'creatable'),
|
isRequired: Object.hasOwn(options, 'required'),
|
toggle: button,
|
selectors: selectors,
|
ui: window.uiFromSelectors(selectors, element),
|
checked: false,
|
};
|
if (!config.taxonomy) return;
|
this.fields.set(fieldId, config);
|
|
//Check for stored selected terms in hidden input
|
this.setSelectedFromValue(input);
|
|
|
if (this.isInitializing) {
|
this.batchFetch.add(config.taxonomy);
|
}
|
this.updateFieldUI(fieldId);
|
|
return fieldId;
|
}
|
|
setSelectedFromValue(fieldId, input) {
|
let selected = new Set();
|
input.value.value.trim()
|
.split(',')
|
.map(id => parseInt(id.trim()))
|
.filter(id => !isNaN(id))
|
.forEach(id => selected.add(id));
|
this.selectedTerms.set(fieldId, selected);
|
}
|
|
addSelected(termId, fieldId = null) {
|
if (!fieldId) fieldId = this.activeField;
|
|
const field = this.fields.get(fieldId);
|
const term = this.store.get(termId);
|
if (!field || !term) return;
|
|
const selected = this.selectedTerms.get(fieldId);
|
if (field.limit !== 0 && selected.size >= field.limit) return;
|
|
selected.add(parseInt(termId));
|
this.addTermToDisplay(termId, fieldId);
|
this.updateFieldValue(fieldId);
|
this.checkLimits(fieldId);
|
}
|
removeSelected(termId, fieldId = null) {
|
if (!fieldId) fieldId = this.activeField;
|
const field = this.fields.get(fieldId);
|
const term = this.store.get(termId);
|
if (!field || !term) return;
|
this.selectedTerms.get(fieldId).delete(parseInt(termId));
|
|
const selectedItem = field.ui.selected.querySelector(`[data-i"${termId}"]`);
|
if (selectedItem) selectedItem.remove();
|
if (this.container.open) {
|
let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
|
if (item) item.remove();
|
}
|
this.updateFieldValue(fieldId);
|
this.checkLimits(fieldId);
|
}
|
updateFieldValue(fieldId) {
|
const field = this.fields.get(fieldId);
|
if (!field) return;
|
let selected = Array.from(this.selectedTerms.get(fieldId));
|
field.ui.value = selected.join(',');
|
}
|
|
checkLimits(fieldId) {
|
if (!this.container.open) return;
|
const field = this.fields.get(fieldId);
|
if (!field || field.limit === 0) return;
|
const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
|
this.setCheckboxes(disabled);
|
}
|
|
updateFieldFromInput(input) {
|
const fieldId = this.getFieldId(input);
|
const field = this.fields.get(fieldId);
|
if (!fieldId || !field) return;
|
|
this.setSelectedFromValue(fieldId, input);
|
this.updateFieldUI(fieldId);
|
}
|
|
updateFieldUI(fieldId) {
|
const field = this.fields.get(fieldId);
|
let selected = this.selectedTerms.get(fieldId);
|
if (!field || selected.size === 0) return;
|
|
Array.from(selected).forEach(termId => {
|
this.addTermToDisplay(termId, fieldId);
|
});
|
}
|
|
updateFieldsForTaxonomy(taxonomy) {
|
let fields = Array.from(this.fields.values())
|
.filter(field => !field.checked && field.taxonomy === taxonomy);
|
const hasItems = Array.from(this.store.data.values())
|
.some(term=>term.taxonomy === taxonomy);
|
|
fields.forEach(field => {
|
field.ui.toggle.disabled = !hasItems && !field.canCreate;
|
field.ui.toggle.title = !hasItems
|
? `No ${field.singular} available`
|
: `Select ${field.plural}`;
|
|
field.checked = true;
|
});
|
}
|
|
showModalTerms(append = true, showPath = false) {
|
const terms = this.store.getFiltered();
|
if (terms.size === 0) return;
|
if (!append) {
|
window.removeChildren(this.ui.terms.list);
|
}
|
|
const currentParent = this.store.filters.parent??0;
|
this.ui.nav.back.hidden = currentParent === 0;
|
|
const fragment = document.createDocumentFragment();
|
terms.forEach(term => {
|
const element = this.createTermElement({
|
show: showPath,
|
... term
|
});
|
if (element) {
|
fragment.appendChild(element);
|
}
|
});
|
|
this.ui.terms.list.append(fragment);
|
}
|
createTermElement(term) {
|
if (!term || !term.name) return null;
|
|
const item = window.getTemplate('termListItem');
|
item.dataset.id = term.id;
|
|
const isSelected = this.selectedTerms.get(this.activeField).has(term.id);
|
let [
|
checkbox,
|
label,
|
nameSpan
|
] = [
|
item.querySelector('input'),
|
item.querySelector('label'),
|
item.querySelector('span, .term-name')
|
];
|
|
let field = this.currentField();
|
let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
|
if (checkbox && label && nameSpan) {
|
[
|
checkbox.id,
|
checkbox.name,
|
checkbox.value,
|
checkbox.disabled,
|
checkbox.checked,
|
label.htmlFor,
|
label.title,
|
label.dataset.path,
|
nameSpan.textContent
|
] = [
|
`${field.element.id}-${term.id}`,
|
`${field.container.id}-${field.taxonomy}-select`,
|
term.id,
|
!isSelected && limitReached,
|
isSelected,
|
`${field.element.id}-${term.id}`,
|
term.path??term.name,
|
term.path,
|
term.show ? term.path : term.name
|
];
|
if (term.hasChildren) {
|
const toggle = window.getTemplate('termChildrenToggle');
|
if (toggle) {
|
toggle.ariaLabel = `View ${field.plural} nested under ${term.name}`;
|
item.append(toggle);
|
}
|
}
|
}
|
|
return item;
|
}
|
|
showAutocompleteTerms() {
|
const field = this.currentField();
|
const terms = this.currentTerms();
|
if (!field || terms.size ===0) return;
|
|
const dropdown = field.ui.dropdown;
|
window.removeChildren(dropdown);
|
if (terms.length === 0) {
|
this.showEmptyState(`No ${field.plural} found.`, dropdown);
|
} else {
|
terms.forEach(term => {
|
const item = this.createAutocompleteTerm(term);
|
if (item) {
|
dropdown.append(item);
|
}
|
})
|
}
|
|
const query = field.ui.search?.value;
|
if (field.canCreate && query.length >= 2 && this.creator) {
|
const createButton = this.createTermButton(query);
|
if (createButton) {
|
dropdown.append(createButton);
|
}
|
}
|
|
dropdown.hidden = false;
|
}
|
createAutocompleteTerm(term) {
|
const item = window.getTemplate('autocompleteItem');
|
if (!item) return;
|
|
item.dataset.id = term.id;
|
item.textContent = term.path || term.name;
|
return item;
|
}
|
/******************************************************************
|
UI
|
******************************************************************/
|
addTermToDisplay(termId, fieldId) {
|
const term = this.store.get(termId);
|
const field = this.fields.get(fieldId);
|
if (!term || !field) return;
|
//if the term already exists in the selected items, bail early
|
if (field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
|
|
const item = window.getTemplate('selectedTerm');
|
if (!item) return;
|
|
item.dataset.id = termId;
|
item.dataset.taxonomy = field.taxonomy;
|
item.querySelector('.item-name').textContent = term.path;
|
item.querySelector('button').title = `Remove ${term.name}`;
|
|
field.ui.selected.append(item);
|
|
if (this.container.open) {
|
this.addTermToModal(termId);
|
const checkbox = this.ui.terms.list.querySelector(`input[value="${termId}"]`);
|
if (checkbox) checkbox.checked = true;
|
}
|
}
|
createTermButton(query) {
|
const button = window.getTemplate('autocompleteButton');
|
if(!button) return;
|
|
let queryEl = button.querySelector('span');
|
queryEl.textContent = `"${query}"`;
|
|
return button;
|
}
|
|
updateBreadcrumbs(termId) {
|
const nav = this.ui.nav.nav;
|
if (!nav) return;
|
const existingCrumb = Array.from(nav.children)
|
.find(crumb => parseInt(crumb.dataset.id) === termId);
|
|
if (existingCrumb) {
|
// Remove all siblings after this crumb
|
let nextSibling = existingCrumb.nextElementSibling;
|
while (nextSibling) {
|
const toRemove = nextSibling;
|
nextSibling = nextSibling.nextElementSibling;
|
toRemove.remove();
|
}
|
} else {
|
// Add new breadcrumb
|
const term = this.store.get(termId);
|
if (!term) return;
|
|
const crumb = window.getTemplate('termBreadcrumb');
|
if (!crumb) return;
|
|
crumb.dataset.id = termId;
|
crumb.textContent = term.name;
|
crumb.title = term.name;
|
|
nav.append(crumb);
|
}
|
}
|
|
updateSelectionCount() {
|
if (!this.container.open) return;
|
const field = this.fields.get(this.activeField);
|
if (!field) return;
|
|
if (this.ui.modal.count) {
|
const total = this.selectedTerms.get(this.activeField).size;
|
|
this.ui.modal.count.textContent = field.limit > 0
|
? `${total} of ${field.limit} ${field.plural} selected`
|
: `${total} ${field.plural} selected`;
|
}
|
|
}
|
/******************************************************************
|
UTILITY
|
******************************************************************/
|
currentField() {
|
return this.fields.get(this.activeField)??false;
|
}
|
currentTerms() {
|
return this.store.getFiltered();
|
}
|
needsCreator() {
|
return Array.from(this.fields.values()).some(field =>
|
field.canCreate || field.hasAutocomplete
|
);
|
}
|
|
getFieldId(element) {
|
if (element.dataset.fieldId) return element.dataset.fieldId;
|
|
const fieldContainer = element.closest('[data-field-id]');
|
return fieldContainer?.dataset.fieldId || null;
|
}
|
|
/**
|
* Sets all checkbox disabled (or not)
|
* @param {Boolean} disabled
|
*/
|
setCheckboxes(disabled) {
|
this.ui.terms.list.querySelectorAll('input[type=checkbox]').forEach(checkbox => {
|
if (!checkbox.checked) {
|
checkbox.disabled = disabled;
|
}
|
});
|
}
|
|
/******************************************************************
|
DATASTORE HELPERS
|
******************************************************************/
|
handleStoreEvent(event, data) {
|
const handlers = {
|
'data-loaded': () => this.handleDataLoaded(),
|
'filters-changed': () => this.handleFiltersChanged(),
|
'fetch-error': () => this.handleFetchError()
|
};
|
|
handlers[event]?.();
|
}
|
handleDataLoaded() {
|
const taxonomy = this.store.filters.taxonomy;
|
if (taxonomy?.includes(',')) {
|
const taxonomies = taxonomy.split(',').map(t => t.trim());
|
taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
|
}
|
|
if (this.container.open) {
|
this.showResults();
|
return;
|
}
|
if (this.activeField) {
|
this.showResults(true);
|
}
|
}
|
|
showResults(isAutoComplete = false) {
|
this.setLoading(false);
|
const terms = this.store.getFiltered();
|
const filters = this.store.filters;
|
const response = this.store.lastResponse?.page || {};
|
const isSearch = filters.search && filters.search.length > 0;
|
const append = filters.page > 1;
|
const field = this.currentField();
|
|
this.notify('terms-loaded', {
|
terms,
|
filters
|
});
|
|
if (terms.length === 0) {
|
if (!append) {
|
this.showEmptyState(isSearch ? `No matching ${field.plural}.` : `No ${field.plural} available.`);
|
}
|
this.observer.unobserve(this.ui.terms.sentinel);
|
} else {
|
if (!isAutoComplete) {
|
this.showModalTerms(append, isSearch);
|
|
if (response.has_more) {
|
this.observer.observe(this.ui.terms.sentinel);
|
} else {
|
this.observer.unobserve(this.ui.terms.sentinel);
|
}
|
} else {
|
this.showAutocompleteTerms()
|
}
|
}
|
|
this.a11y.announce(terms.length, append);
|
}
|
handleFiltersChanged() {
|
// if (this.modal?.open) {
|
// this.setLoading();
|
// }
|
}
|
|
handleFetchError(error) {
|
this.setLoading(false);
|
}
|
async batchFetchTaxonomies() {
|
if (this.batchFetch.size === 0) return;
|
|
const taxonomies = Array.from(this.batchFetch);
|
taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
|
this.batchFetch.clear();
|
|
try {
|
taxonomies.forEach(tax => this.loadedTaxonomies.add(tax));
|
|
await this.store.setFilters({
|
taxonomy: taxonomies.join(','),
|
page: 1,
|
search: '',
|
parent: 0
|
});
|
} catch (error) {
|
console.error('Failed to batch fetch taxonomies:', error);
|
}
|
}
|
|
preloadTaxonomy(taxonomy) {
|
if (this.loadedTaxonomies.has(taxonomy)) return;
|
|
this.store.setFilters( {
|
taxonomy: taxonomy,
|
page: 1,
|
search: '',
|
parent: 0
|
});
|
|
this.loadedTaxonomies.add(taxonomy);
|
}
|
|
/**************************************************
|
LOADING
|
**************************************************/
|
setLoading(on = true) {
|
this.ui.loading.loading.hidden = on;
|
this.modal.classList.toggle('loading', on);
|
|
if (on) {
|
let searchQuery = this.store.filters.search || '';
|
searchQuery = searchQuery === '' ? false : searchQuery;
|
const currentParent = this.store.filters.parent || 0;
|
const message = searchQuery
|
? `Searching for "${searchQuery} items` :
|
currentParent === 0
|
? 'loading items'
|
: 'loading child items';
|
|
if (window.typeLoop && this.ui.loading.text) {
|
this.stopTyping = window.typeLoop(this.ui.loading.text, message);
|
} else {
|
this.ui.loading.text.textContenet = message;
|
}
|
} else {
|
if (this.stopTyping) {
|
this.stopTyping();
|
this.stopTyping = null;
|
}
|
}
|
}
|
showEmptyState(message = 'No items found.', container = null) {
|
if (!container) container = this.ui.terms.list;
|
const emptyElement = window.getTemplate('noTermResults');
|
const span = emptyElement.querySelector('span');
|
if (message && span) {
|
span.textContent = message;
|
}
|
container.append(emptyElement);
|
}
|
/**************************************************
|
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);
|
}
|
});
|
}
|
/******************************************************
|
CLEANUP
|
******************************************************/
|
destroy() {
|
document.removeEventListener('click', this.clickHandler);
|
document.removeEventListener('change', this.changeHandler);
|
document.removeEventListener('input', this.inputHandler);
|
document.removeEventListener('focus', this.focusHandler);
|
document.removeEventListener('blur', this.blurHandler);
|
|
this.observer?.disconnect();
|
this.subscribers.clear();
|
this.fields.clear();
|
this.selectedTerms.clear();
|
}
|
}
|
|
document.addEventListener('DOMContentLoaded', function() {
|
window.auth.subscribe((event) => {
|
if (event === 'auth-loaded') {
|
window.jvbSelector = new TaxonomySelector();
|
}
|
});
|
});
|