From d7e7d248cbe41cd7a9ef9c2fb022b6c4831f99a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 31 May 2026 15:22:56 +0000
Subject: [PATCH] =jakevan complete
---
assets/js/concise/TaxonomySelector.js | 615 +++++++++++++++++++++++++++++++++++++------------------
1 files changed, 409 insertions(+), 206 deletions(-)
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 0565dee..90f6076 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -1,3 +1,4 @@
+
class TaxonomySelector {
constructor() {
this.container = document.querySelector('dialog#jvb-selector');
@@ -13,6 +14,7 @@
this.activeField = null;
this.isInitializing = true;
+ this.lazyInit = false;
this.messageText = {}
this.init();
}
@@ -20,6 +22,7 @@
init() {
this.initStore();
this.initElements();
+ this.defineTemplates();
this.initModal();
this.scanExistingFields();
this.initListeners();
@@ -41,7 +44,7 @@
indexes: [
{name: 'taxonomy', keyPath: 'taxonomy'},
{name: 'parent', keyPath: 'parent'},
- {name: 'slug', keyPath: 'slug', unique: true},
+ {name: 'slug', keyPath: 'slug'},
{name: 'count', keyPath: 'count'},
],
endpoint: 'terms',
@@ -61,13 +64,95 @@
this.store.subscribe(this.handleStoreEvent.bind(this));
}
+ defineTemplates() {
+ const T = window.jvbTemplates;
+ const terms = this;
+
+ T.define('emptyState');
+ T.define('selectedTerm', {
+ refs: {
+ name: '.item-name',
+ btn: 'button',
+ },
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.dataset.taxonomy = data.taxonomy;
+ if (refs.name) refs.name.textContent = data.path;
+ if (refs.button) refs.button.title = `Remove ${data.name}`;
+ }
+ });
+ T.define('termListItem', {
+ refs: {
+ checkbox: 'input',
+ label: 'label',
+ name: 'span, .term-name'
+ },
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+
+ let field = terms.currentField();
+ let isSelected = terms.selectedTerms.get(terms.activeField).has(data.id);
+ let limitReached = field.limit > 0 && terms.selectedTerms.get(terms.activeField).size >= field.limit;
+
+ if (refs.checkbox) {
+ refs.checkbox.dataset.id = data.id;
+ refs.checkbox.id = `${field.id}-${data.id}`;
+ refs.checkbox.name = `${field.id}-${field.taxonomy}-select`;
+ refs.checkbox.value = data.id;
+ refs.checkbox.disabled = !isSelected && limitReached;
+ refs.checkbox.checked = isSelected;
+ }
+ if (refs.label) {
+ refs.label.htmlFor = `${field.id}-${data.id}`;
+ refs.label.title = data.path??data.name;
+ refs.label.dataset.path = data.path;
+ }
+ if (refs.name) {
+ refs.name.textContent = data.show ? data.path : data.name;
+ }
+
+ if (data.hasChildren) {
+ let temp = {
+ plural: field.plural,
+ name: data.name
+ };
+ const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
+ el.append(toggle);
+ }
+ }
+ });
+
+ T.define('termChildrenToggle', {
+ setup({el, refs, manyRefs, data}) {
+ el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
+ }
+ });
+
+ T.define('termBreadcrumb', {
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.textContent = data.name;
+ el.title = data.name;
+ }
+ });
+
+ T.define('autocompleteItem', {
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.textContent = data.path||data.name;
+ el.title = `Select ${data.name}`;
+ }
+ });
+
+
+ }
/******************************************************************
ELEMENTS
******************************************************************/
initElements() {
this.selectors = {
search: {
- input: '[type=search]',
+ input: '[type="search"]',
clear: '.clear-search',
container: '.search-wrapper',
results: '.search-results',
@@ -99,7 +184,7 @@
},
favourites: '.favourite-terms',
field: {
- toggle: 'button.taxonomy-toggle',
+ toggle: 'button.selector-toggle, [data-filter="taxonomy"]',
value: 'input[type="hidden"]',
selected: '.selected-items',
dropdown: {
@@ -147,29 +232,18 @@
}
handleClick(e) {
- const fieldId = (this.container.open) ? this.activeField : this.getFieldId(e.target);
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
+ const fieldId = this.getFieldId(e.target) || this.activeField;
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.wrapper) {
- field.ui.dropdown.wrapper.hidden = true;
+ if (this.creator) {
+ let button = window.targetCheck(e, this.selectors.create.button);
+ if (button) {
+ this.maybeCreateTerm(e).then(()=>{});
}
-
- if (field.ui.search) {
- field.ui.search.value = '';
- }
- }
-
- const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
-
- if (toggleButton) {
- e.preventDefault();
- this.openModal(fieldId);
- return;
}
const removeButton = window.targetCheck(e, '.remove-term');
@@ -181,6 +255,27 @@
return;
}
+ const autocomplete = window.targetCheck(e, '.item.autocomplete');
+
+ if (autocomplete) {
+ let termId = parseInt(autocomplete.dataset.id);
+ this.addSelected(termId, fieldId);
+ this.scheduleHideDropdown(fieldId, 6000);
+ if (field.ui.search) {
+ field.ui.search.value = '';
+ }
+ return;
+ }
+
+ const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
+
+ if (toggleButton) {
+ e.preventDefault();
+ this.openModal(fieldId);
+ return;
+ }
+
+
if (e.target.matches('.modal-close')) {
this.updateFieldValue(fieldId);
this.modal?.handleClose();
@@ -208,12 +303,14 @@
if (pathLevel) {
const termId = parseInt(pathLevel.dataset.id)??0;
this.navigateTo(termId);
+ return;
}
const dropdown = window.targetCheck(e, this.selectors.field.dropdown);
if (dropdown) {
// reset the timer for hiding the dropdown
this.scheduleHideDropdown(fieldId);
+ return;
}
const clearSearch = window.targetCheck(e, this.selectors.search.clear);
@@ -231,20 +328,12 @@
this.ui.search.input.value = '';
}
}
-
- if (this.creator) {
- let button = window.targetCheck(e, this.selectors.create.button);
- if (button) {
- this.maybeCreateTerm(e).then(()=>{});
- }
- }
-
}
handleChange(e) {
- if (!this.container.contains(e.target)) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
return;
}
- if (e.target.type !== 'checkbox') return;
+ if (!['checkbox', 'button'].includes(e.target.type)) return;
e.preventDefault();
e.stopPropagation();
@@ -258,11 +347,14 @@
}
//For search in modal or field autocomplete
handleInput(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
let fieldId = this.getFieldId(e.target)??this.activeField;
if (!fieldId) return;
const field = this.fields.get(fieldId);
if (!field) return;
- if (e.target.type === 'checkbox') return;
+ if (['checkbox', 'button'].includes(e.target.type)) return;
e.preventDefault();
e.stopPropagation();
@@ -273,7 +365,7 @@
}
let query = e.target.value.trim();
- this.setMessage(true, `Searching for "${query}" in ${field.plural??'items'}`);
+ this.setMessage(field,true, `Searching for "${query}" in ${field.plural??'items'}`);
window.debouncer.schedule(
`${fieldId}-search`,
async () => {
@@ -298,7 +390,7 @@
return;
}
this.activeField = fieldId;
- this.setMessage(true, `Loading ${field.plural}...`);
+ this.setMessage(field,true, `Loading ${field.plural}...`);
this.resetFilters({taxonomy: field.taxonomy});
}
@@ -316,9 +408,13 @@
}
handleFocus(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
const fieldId = this.getFieldId(e.target);
+ if (!fieldId) return;
const field = this.fields.get(fieldId);
- if (!fieldId || !field) return;
+ if (!field) return;
if (!field.hasAutocomplete && !field.hasSearch) return;
window.debouncer.cancel(`${fieldId}-search-results`);
@@ -330,15 +426,22 @@
//Hide autocomplete dropdown on blur
handleBlur(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
const fieldId = this.getFieldId(e.target);
+ if (!fieldId) return;
const field = this.fields.get(fieldId);
- if (!fieldId || ! field) return;
+ if (!field) return;
if (!field.hasAutocomplete || this.container.open) return;
+ if (e.target.closest('.remove-item')) return;
+
+ if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
this.scheduleHideDropdown(fieldId);
}
- scheduleHideDropdown(fieldId){
+ scheduleHideDropdown(fieldId, delay = 1500){
const field = this.fields.get(fieldId);
if (!field) return;
@@ -352,7 +455,7 @@
field.ui.dropdown.wrapper.hidden = true;
}
},
- 1500
+ delay
);
}
@@ -372,7 +475,9 @@
);
this.modal.subscribe((event, data) => {
switch (event) {
-
+ case 'modal-close':
+ this.closeModal()
+ break;
}
});
}
@@ -393,7 +498,7 @@
if (!field) return;
this.setField(fieldId);
- this.ui.modal.title.textContent = `Select ${field.plural}`;
+ this.ui.modal.title.textContent = (field.isFilter) ?`Filter by ${field.singular}` : `Select ${field.plural}`;
if (this.ui.search.container) {
this.ui.search.container.hidden = !field.canSearch;
}
@@ -402,23 +507,85 @@
}
let message = `Opened ${field.singular} selection. Choose from checkboxes, or search to filter results.`;
+ window.removeChildren(this.ui.selected);
window.removeChildren(this.ui.terms.list);
this.modal.handleOpen();
this.a11y.announce(message);
}
+ openEmpty(taxonomy, singular, plural, onComplete) {
+ // Store the callback for when modal closes
+ this.emptyCallback = onComplete;
+
+ // Create a temporary "field" for bulk operations
+ const bulkFieldId = `empty-${taxonomy}-${Date.now()}`;
+
+ if (!this.fields.has(bulkFieldId)) {
+ this.fields.set(bulkFieldId, {
+ id: bulkFieldId,
+ taxonomy: taxonomy,
+ singular: singular,
+ plural: plural,
+ canSearch: true,
+ canCreate: false,
+ hasAutocomplete: false,
+ isFilter: false,
+ isEmpty: true,
+ limit: 0,
+ ui: {},
+ element: null,
+ value: null,
+ toggle: null,
+ checked: true
+ });
+ this.selectedTerms.set(bulkFieldId, new Set());
+ }
+
+ this.setField(bulkFieldId);
+ this.ui.modal.title.textContent = `Add to ${plural}`;
+
+ if (this.ui.search?.container) {
+ this.ui.search.container.hidden = false;
+ }
+
+ window.removeChildren(this.ui.selected);
+ window.removeChildren(this.ui.terms.list);
+
+ this.modal.handleOpen();
+ }
+
closeModal() {
- this.modal.handleClose();
const field = this.fields.get(this.activeField);
if (!field) return;
+
+
+ this.updateFieldValue(this.activeField);
+
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
- });
+ if (field.isEmpty && this.emptyCallback) {
+ const selectedTermIds = Array.from(this.selectedTerms.get(this.activeField) || []);
+ const selectedTerms = selectedTermIds.map(id => this.store.get(id)).filter(Boolean);
+
+ this.emptyCallback({
+ taxonomy: field.taxonomy,
+ termIds: selectedTermIds,
+ terms: selectedTerms
+ });
+
+ // Cleanup temporary bulk field
+ this.fields.delete(this.activeField);
+ this.selectedTerms.delete(this.activeField);
+ this.emptyCallback = null;
+ this.bulkAssignmentTaxonomy = null;
+ } else {
+ this.notify('selected-terms', {
+ terms: this.selectedTerms.get(this.activeField),
+ taxonomy: field.taxonomy
+ });
+ }
this.activeField = null;
@@ -462,24 +629,26 @@
if (!field) return;
if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
- const item = window.getTemplate('selectedTerm');
- if (!item) return;
+ this.ui.selected.append(this.getSelectedTermUI(term));
+ }
- item.dataset.id = termId;
- item.dataset.taxonomy = field.taxonomy;
- item.querySelector('.item-name').textContent = term.path;
- item.querySelector('button').title = `Remove ${term.name}`;
-
- this.ui.selected.append(item);
+ getSelectedTermUI(term, showPath = true) {
+ return window.jvbTemplates.create('selectedTerm', term);
}
/******************************************************************
FIELDS
******************************************************************/
scanExistingFields(container = document.body) {
- container.querySelectorAll('[data-type="selector"]').forEach(
+ container.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach(
selector => {
try {
- this.registerField(selector);
+ if (selector.dataset.lazy) {
+ this.lazyInit = true;
+ } else {
+ // Register field if not already registered
+ // registerField will check if already registered and return early if so
+ this.registerField(selector);
+ }
} catch (error) {
this.error.log(error, {
component: 'TaxonomySelector',
@@ -489,12 +658,41 @@
}
}
);
+ if (this.lazyInit) {
+ this.initObserver(container);
+ }
+ }
+
+ unregisterFields(container) {
+ container.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach(
+ selector=> {
+ this.fields.delete(selector.dataset.fieldId);
+ }
+ );
+ }
+ initObserver(container){
+ this.lazyObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting && entry.target.dataset.lazy) {
+ delete entry.target.dataset.lazy;
+ this.registerField(entry.target);
+ this.lazyObserver.unobserve(entry.target);
+ }
+ });
+ }, {rootMargin: '50px'});
+
+ container.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach(field => {
+ this.lazyObserver.observe(field);
+ });
}
registerField(element, options = {}) {
+ if (element.dataset.fieldId && this.fields.has(element.dataset.fieldId)) {
+ return element.dataset.fieldId; // Already registered
+ }
+
let input = element.querySelector('input[type="hidden"]');
- if (!input) {
- console.warn('TaxonomySelector: No hidden input found for field', element);
+ if (!input && !Object.hasOwn(element.dataset, 'filter')) {
return;
}
@@ -505,7 +703,8 @@
let selectors = this.selectors.field;
- let button = element.querySelector('button.taxonomy-toggle');
+ const isFilter = Object.hasOwn(element.dataset,'filter') && element.dataset.filter === 'taxonomy';
+ let button = (isFilter) ? element : element.querySelector('button.selector-toggle');
if (Object.keys(options).length === 0){
if (!button) return;
@@ -517,7 +716,6 @@
autocomplete: Object.hasOwn(button.dataset, 'autocomplete'),
creatable: Object.hasOwn(button.dataset, 'creatable')
};
- if (Object.keys(options).length === 0) return;
} else if (Object.hasOwn(options, 'toggle')) {
button = document.querySelector(options.toggle);
selectors.toggle = options.toggle;
@@ -536,6 +734,7 @@
hasAutocomplete: options.autocomplete??false,
canCreate: options.creatable??false,
isRequired: options.required??false,
+ isFilter: isFilter,
toggle: button,
create: {
button: null,
@@ -545,6 +744,10 @@
ui: window.uiFromSelectors(selectors, element),
checked: false,
};
+
+ if (isFilter && !config.ui.toggle) {
+ config.ui.toggle = element;
+ }
if (!config.taxonomy) {
console.error('TaxonomySelector: Field missing taxonomy', element);
return;
@@ -563,23 +766,36 @@
if (this.isInitializing) {
this.batchFetch.add(config.taxonomy);
}
- this.updateFieldUI(fieldId);
+
+ if (element.offsetParent !== null) {
+ this.updateFieldUI(fieldId);
+ } else {
+ // Defer until visible
+ requestIdleCallback(() => {
+ if (element.offsetParent !== null) {
+ this.updateFieldUI(fieldId);
+ }
+ }, {timeout: 2000});
+
+ }
return fieldId;
}
setSelectedFromValue(fieldId, input) {
- if (!input) return;
if (!fieldId) return;
let field = this.fields.get(fieldId);
if (!field) return;
+ if (!input && !field.isFilter) return;
let selected = new Set();
- input.value.trim()
- .split(',')
- .map(id => parseInt(id.trim()))
- .filter(id => !isNaN(id))
- .forEach(id => selected.add(id));
+ if (input) {
+ input.value.trim()
+ .split(',')
+ .map(id => parseInt(id.trim()))
+ .filter(id => !isNaN(id))
+ .forEach(id => selected.add(id));
+ }
this.selectedTerms.set(fieldId, selected);
}
@@ -594,7 +810,7 @@
if (field.limit !== 0 && selected.size >= field.limit) return;
selected.add(parseInt(termId));
- if (!this.container.open) {
+ if (!this.container.open && !field.isFilter) {
this.updateFieldValue(fieldId);
}
this.addTermToDisplay(termId, fieldId);
@@ -607,17 +823,17 @@
if (!field || !term) return;
this.selectedTerms.get(fieldId).delete(parseInt(termId));
- const selectedItem = field.ui.selected.querySelector(`[data-id="${termId}"]`);
+ const selectedItem = (field.ui.selected) ? field.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
if (selectedItem) selectedItem.remove();
if (this.container.open) {
- let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
+ let item = (this.ui.selected) ? this.ui.selected.querySelector(`[data-id="${termId}"]`) : false;
if (item) item.remove();
let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
if (checkbox) {
checkbox.checked = false;
}
}
- if (!this.container.open) {
+ if (!this.container.open && !field.isFilter) {
this.updateFieldValue(fieldId);
}
@@ -627,13 +843,16 @@
const field = this.fields.get(fieldId);
if (!field) return;
let selected = Array.from(this.selectedTerms.get(fieldId));
- field.ui.value.value = selected.join(',');
+ if (field.ui.value) {
+ field.ui.value.value = selected.join(',')??'';
+ field.ui.value.dispatchEvent(new Event('change', { bubbles: true }));
+ }
}
checkLimits(fieldId) {
if (!this.container.open) return;
const field = this.fields.get(fieldId);
- if (!field || field.limit === 0) return;
+ if (!field || !field.isFilter || field.limit === 0) return;
const disabled = this.selectedTerms.get(fieldId).size >= field.limit;
this.setCheckboxes(disabled);
}
@@ -651,7 +870,7 @@
updateFieldUI(fieldId) {
const field = this.fields.get(fieldId);
let selected = this.selectedTerms.get(fieldId)??new Set();
- if (!field || selected.size === 0) return;
+ if (!field || field.isFilter || selected.size === 0) return;
Array.from(selected).forEach(termId => {
this.addTermToDisplay(termId, fieldId);
@@ -660,13 +879,14 @@
updateFieldsForTaxonomy(taxonomy) {
let fields = Array.from(this.fields.values())
- .filter(field => !field.checked && field.taxonomy === taxonomy);
+ .filter(field => field.taxonomy === taxonomy);
const hasItems = Array.from(this.store.data.values())
- .some(term=>term.taxonomy === taxonomy);
+ .some(term => term && term.taxonomy === taxonomy);
fields.forEach(field => {
- field.ui.toggle.disabled = !hasItems && !field.canCreate;
- field.ui.toggle.title = !hasItems
+ if (!field.toggle) return;
+ field.toggle.disabled = !hasItems && !field.canCreate;
+ field.toggle.title = !hasItems
? `No ${field.singular} available`
: `Select ${field.plural}`;
@@ -681,15 +901,18 @@
if (this.store.filters.page??1 === 1) {
window.removeChildren(this.ui.terms.list);
}
- this.setMessage(true, this.store.filters.search === ''
+ this.setMessage(field,true, this.store.filters.search === ''
? `No matching ${field.plural}.`
: `No ${field.plural} found.`,
false);
if (this.ui.terms.sentinel) {
this.observer.unobserve(this.ui.terms.sentinel);
}
+ return;
}
+ this.setCreateButton(field,true);
+
if (this.ui.terms.sentinel) {
if (this.store.lastResponse?.has_more) {
this.observer.observe(this.ui.terms.sentinel);
@@ -701,109 +924,49 @@
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.append(element);
- }
- });
+ window.chunkIt(
+ terms,
+ (term) => this.createTermElement({show:showPath, ... term}),
+ (fragment) => this.ui.terms.list.append(fragment),
+ 10
+ ).then(()=>{});
- this.setMessage(false);
-
- this.ui.terms.list.append(fragment);
+ if (terms.length > 0) {
+ this.setMessage(field,false);
+ }
}
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.dataset.id,
- checkbox.id,
- checkbox.name,
- checkbox.value,
- checkbox.disabled,
- checkbox.checked,
- label.htmlFor,
- label.title,
- label.dataset.path,
- nameSpan.textContent
- ] = [
- term.id,
- `${field.element.id}-${term.id}`,
- `${field.element.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;
+ return window.jvbTemplates.create('termListItem', term);
}
showAutocompleteTerms() {
const field = this.currentField();
- const terms = this.currentTerms();
- if (!field) return;
-
+ if (!field || !field.hasAutocomplete || !field.ui.dropdown?.list) return;
const dropdown = field.ui.dropdown.list;
- if (!dropdown) return;
+ const terms = this.currentTerms();
window.removeChildren(dropdown);
if (terms.length === 0) {
- this.setMessage(true, `No ${field.plural} found.`, false);
+ this.setMessage(field,true, `No ${field.plural} found.`, false);
} else {
- terms.forEach(term => {
- const item = this.createAutocompleteTerm(term);
- if (item) {
- dropdown.append(item);
- }
- });
- this.setMessage(false);
- }
- this.setCreateButton(true);
+ window.chunkIt(
+ terms,
+ (term) => this.createAutocompleteTerm(term),
+ (fragment) => dropdown.append(fragment)
+ ).then(()=>{});
- if (field.ui.dropdown?.wrapper) {
+ this.setMessage(field,false);
+ }
+ this.setCreateButton(field,true);
+
+ if (field.ui.dropdown.wrapper) {
field.ui.dropdown.wrapper.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;
+ createAutocompleteTerm(term) {
+ return window.jvbTemplates.create('autocompleteItem', term);
}
/******************************************************************
UI
@@ -812,18 +975,16 @@
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;
+ if (field.ui.selected && 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}`;
+ let item = this.getSelectedTermUI(term);
- field.ui.selected.append(item);
+ if (field.ui.selected) {
+ field.ui.selected.append(item);
+ }
if (this.container.open) {
this.addTermToModal(termId);
@@ -850,13 +1011,7 @@
// 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;
+ const crumb = window.jvbTemplates.create('termBreadcrumb', term);
nav.append(crumb);
}
@@ -879,6 +1034,13 @@
/******************************************************************
UTILITY
******************************************************************/
+ checkRendered(collection, term) {
+ if (!collection) return;
+ if (!Object.hasOwn(collection, term.taxonomy)) {
+ collection[term.taxonomy] = new Map();
+ }
+ return collection[term.taxonomy].has(term.id);
+ }
currentField() {
return this.fields.get(this.activeField)??false;
}
@@ -924,12 +1086,12 @@
handlers[event]?.(data);
} catch (error) {
console.error(`Error handling store event "${event}":`, error);
- this.setMessage(true, 'An error occurred loading data', false);
}
}
handleDataLoaded() {
const taxonomy = this.store.filters.taxonomy;
- if (taxonomy?.includes(',')) {
+
+ if (taxonomy) {
const taxonomies = taxonomy.split(',').map(t => t.trim());
taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax));
}
@@ -942,11 +1104,9 @@
this.showResults(true);
return;
}
- this.setMessage(false);
}
showResults(isAutoComplete = false) {
- this.setMessage(false);
const terms = this.store.getFiltered();
const filters = this.store.filters;
const isSearch = filters.search && filters.search.length > 0;
@@ -956,7 +1116,10 @@
filters
});
-
+ if (!this.activeField && isAutoComplete) {
+ return;
+ }
+ this.setMessage(this.currentField(), false);
if (isAutoComplete) {
this.showAutocompleteTerms();
} else {
@@ -972,19 +1135,37 @@
handleFetchError(error) {
const field = this.currentField();
- const message = field
- ? `Failed to load ${field.plural}`
- : 'Failed to load data';
+ this.setMessage(field, true, 'Something went wrong.', false);
- this.setMessage(true, message, false);
+ const conf = this.container.open || field?.isFilter ? this.ui : field?.ui;
+ const p = conf?.message?.message;
+
+ if (p && !p.querySelector('.clear-cache-btn')) {
+ const btn = document.createElement('button');
+ btn.className = 'clear-cache-btn';
+ btn.type = 'button';
+ btn.textContent = 'Clear cache and try again';
+ btn.addEventListener('click', async () => {
+ btn.remove();
+ this.store.clearCache();
+ if (this.activeField && field) {
+ await this.store.setFilters({
+ taxonomy: field.taxonomy,
+ page: 1,
+ search: '',
+ parent: 0
+ });
+ }
+ });
+ p.appendChild(btn);
+ }
+
console.error('Store fetch error:', error);
}
async batchFetchTaxonomies() {
if (this.batchFetch.size === 0) return;
-
const taxonomies = Array.from(this.batchFetch);
this.batchFetch.clear();
-
try {
await this.store.setFilters({
taxonomy: taxonomies.join(','),
@@ -1009,15 +1190,14 @@
/**************************************************
LOADING
**************************************************/
- setCreateButton(show = true) {
- const field = this.currentField();
- if (!field || !field.canCreate || !this.creator) return;
+ setCreateButton(field, show = true) {
+ if (!field.canCreate || !this.creator) return;
const conf = (this.container.open) ? this.ui : field.ui;
-
if (!conf.create?.button || !conf.create?.span) return;
const createButton = conf.create.button;
+ createButton.hidden = !show;
const buttonSpan = conf.create.span;
const input = (this.container.open) ? conf.search.input : conf.search;
if (!input) return;
@@ -1037,48 +1217,71 @@
if (!field) return;
window.debouncer.cancel(`${field.id}-search-results`);
+
let data = {
taxonomy: field.taxonomy,
parent: this.store.filters.parent??0
}
- //If it's autocomplete or the selector's search input, we just need the name
+
if (!this.container.open || this.ui.search.input.value !== '') {
data.name = (this.container.open) ? this.ui.search.input.value : field.ui.search.value;
} else {
- //Otherwise, we've created it from the details element
data.parent = this.creator.ui.parent.value??data.parent;
data.name = this.creator.ui.name.value??false;
}
+
if (data.parent !== undefined && data.name) {
- this.setMessage(true, `Creating "${data.name}"...`);
- this.setCreateButton(false);
+ this.setMessage(field,true, `Creating "${data.name}"...`);
+ this.setCreateButton(field,false);
+
if (this.container.open) {
window.removeChildren(this.ui.terms.list);
} else {
field.ui.search.disabled = true;
- window.removeChildren(field.ui.dropdown.list);
if (field.ui.dropdown.wrapper) {
field.ui.dropdown.wrapper.hidden = false;
}
}
+
let term = await this.creator.handleTermCreation(data);
+
if (term) {
+ // Stop any typeLoop animation and show success message WITHOUT typeLoop
+ this.setMessage(field,true, `"${term.name}" created!`, false);
+
this.addSelected(term.id, field.id);
+ this.updateFieldValue(field.id);
+ // For autocomplete, show the newly created term in dropdown
+ if (!this.container.open && field.ui.dropdown.list) {
+ window.removeChildren(field.ui.dropdown.list);
+ const termElement = this.createAutocompleteTerm(term);
+ if (termElement) {
+ termElement.classList.add('newly-created');
+ field.ui.dropdown.list.append(termElement);
+ }
+ }
+ this.scheduleHideDropdown(field.id, 300);
+ this.setMessage(field,false);
+ } else {
+ // Creation failed - hide immediately
+ this.setMessage(field,false);
+ if (!this.container.open && field.ui.dropdown.wrapper) {
+ field.ui.dropdown.wrapper.hidden = true;
+ }
}
+
if (!this.container.open) {
field.ui.search.disabled = false;
field.ui.search.value = '';
}
- this.scheduleHideDropdown(field.id);
- this.setMessage(false);
}
}
- setMessage(show = true, message = '', type = true) {
- const field = this.currentField();
- if (!field) return;
+ setMessage(field, show = true, message = '', type = true) {
+ const conf = this.container.open||field.isFilter ? this.ui : (field.isFilter ? null : field.ui);
+ if (!conf?.message?.message) return;
+
message = (message === '') ? `No ${field.plural??'items'} found.` : message;
- const conf = (this.container.open) ? this.ui : field.ui;
const p = conf.message.message;
const pText = conf.message.text;
@@ -1142,6 +1345,7 @@
this.observer?.unobserve(this.ui.terms.sentinel);
}
this.observer?.disconnect();
+ this.lazyObserver?.disconnect();
// Remove event listeners
document.removeEventListener('click', this.clickHandler);
@@ -1176,4 +1380,3 @@
}
});
});
-
--
Gitblit v1.10.0