From 2c955cebb5f1e01fbdb866b50d296fe9fbd852b8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 06 Jan 2026 20:40:03 +0000
Subject: [PATCH] =TaxonomySelector.js and creator refactor complete
---
assets/js/concise/TaxonomySelector.js | 499 +++++++++++++++++++++++++++++++++++++------------------
1 files changed, 336 insertions(+), 163 deletions(-)
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index fcd9364..0565dee 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -9,11 +9,11 @@
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.messageText = {}
this.init();
}
@@ -70,7 +70,11 @@
input: '[type=search]',
clear: '.clear-search',
container: '.search-wrapper',
- results: '.search-results'
+ results: '.search-results',
+ },
+ create: {
+ button: 'button.submit-term',
+ span: '.submit-term span',
},
terms: {
list: '.items-container',
@@ -83,9 +87,9 @@
child: '.toggle-children',
pathLevel: '.path-level',
},
- loading: {
- loading: '.loading',
- text: '.loading span',
+ message: {
+ message: 'p.message',
+ text: 'p.message span',
},
selected: '.selected-items',
modal: {
@@ -98,12 +102,23 @@
toggle: 'button.taxonomy-toggle',
value: 'input[type="hidden"]',
selected: '.selected-items',
- dropdown: '.search-results',
- search: '[data-autocomplete]',
+ dropdown: {
+ list: '.search-results',
+ wrapper: '.auto-wrapper',
+ },
+ create: {
+ button: '.auto-wrapper .submit-term',
+ span: '.auto-wrapper button span',
+ },
+ search: 'input[data-autocomplete]',
+ message: {
+ message: 'p.message',
+ text: 'p.message span',
+ },
}
}
- this.ui = window.uiFromSelectors(this.selectors);
+ this.ui = window.uiFromSelectors(this.selectors, this.container);
}
initListeners() {
@@ -132,7 +147,7 @@
}
handleClick(e) {
- const fieldId = this.getFieldId(e.target);
+ const fieldId = (this.container.open) ? this.activeField : this.getFieldId(e.target);
const field = this.fields.get(fieldId);
if (!fieldId || !field) return;
@@ -140,8 +155,8 @@
if (autoComplete) {
let termId = parseInt(autoComplete.dataset.id);
this.addSelected(termId, fieldId);
- if (field.ui.dropdown) {
- field.ui.dropdown.hidden = true;
+ if (field.ui.dropdown.wrapper) {
+ field.ui.dropdown.wrapper.hidden = true;
}
if (field.ui.search) {
@@ -149,24 +164,25 @@
}
}
- const toggleButton = window.targetCheck(e, field.ui.toggle);
+ const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
+
if (toggleButton) {
e.preventDefault();
this.openModal(fieldId);
return;
}
- const removeButton = window.targetCheck(e, 'button.remove-item');
+ const removeButton = window.targetCheck(e, '.remove-term');
if (removeButton) {
- const fieldId = this.getFieldId(removeButton);
- const termId = removeButton.closest('.selected-item').dataset.id??false;
+ const termId = removeButton.closest('[data-id]').dataset.id??false;
if (fieldId && termId) {
- this.removeSelected(termId, fieldId);
+ this.removeSelected(parseInt(termId), fieldId);
}
return;
}
if (e.target.matches('.modal-close')) {
+ this.updateFieldValue(fieldId);
this.modal?.handleClose();
return;
}
@@ -194,11 +210,10 @@
this.navigateTo(termId);
}
- const dropdown = window.targetCheck(e, field.selectors.dropdown);
+ 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);
@@ -217,6 +232,13 @@
}
}
+ 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)) {
@@ -240,29 +262,59 @@
if (!fieldId) return;
const field = this.fields.get(fieldId);
if (!field) return;
+ if (e.target.type === 'checkbox') return;
+ e.preventDefault();
+ e.stopPropagation();
+
+ //If it's the autocomplete field, we need to set the active field
if (!this.container.open) {
- this.activeField = fieldId;
+ this.setField(fieldId);
}
- const query = e.target.value.trim();
+ let query = e.target.value.trim();
+ this.setMessage(true, `Searching for "${query}" in ${field.plural??'items'}`);
window.debouncer.schedule(
`${fieldId}-search`,
async () => {
+ if (this.container.open) {
+ window.removeChildren(this.ui.terms.list);
+ }
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
);
}
+ setField(fieldId) {
+ const field = this.fields.get(fieldId);
+ if (!field) {
+ console.error('No field found...');
+ return;
+ }
+ this.activeField = fieldId;
+ this.setMessage(true, `Loading ${field.plural}...`);
+ this.resetFilters({taxonomy: field.taxonomy});
+ }
+
+ resetFilters(filters) {
+ if (!Object.hasOwn(filters, 'taxonomy')) {
+ return;
+ }
+ filters = {
+ page: 1,
+ search: '',
+ parent: 0,
+ ... filters
+ };
+ this.store.setFilters(filters);
+ }
+
handleFocus(e) {
const fieldId = this.getFieldId(e.target);
const field = this.fields.get(fieldId);
@@ -272,8 +324,7 @@
window.debouncer.cancel(`${fieldId}-search-results`);
if (!this.container.open){
- this.activeField = fieldId;
- this.preloadTaxonomy(field.taxonomy);
+ this.setField(fieldId);
}
}
@@ -282,7 +333,7 @@
const fieldId = this.getFieldId(e.target);
const field = this.fields.get(fieldId);
if (!fieldId || ! field) return;
- if (!field.hasAutocomplete) return;
+ if (!field.hasAutocomplete || this.container.open) return;
this.scheduleHideDropdown(fieldId);
}
@@ -294,8 +345,12 @@
window.debouncer.schedule(
`${fieldId}-search-results`,
() => {
- this.activeField = null;
- field.ui.dropdown.hidden = true;
+ if (!this.container.open) {
+ this.activeField = null;
+ }
+ if (field.ui.dropdown.wrapper) {
+ field.ui.dropdown.wrapper.hidden = true;
+ }
},
1500
);
@@ -312,7 +367,6 @@
this.container,
{
handleForm: false,
- save: null,
open: null
}
);
@@ -338,36 +392,18 @@
const field = this.fields.get(fieldId);
if (!field) return;
- this.activeField = fieldId;
+ this.setField(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`;
- }
+ if (this.creator) {
+ this.creator.handleOpen(field);
}
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);
}
@@ -394,7 +430,10 @@
const current = this.store.filters.parent;
if (current === 0) return;
let term = this.store.get(parseInt(current));
- if (!term) return;
+ if (!term) {
+ this.navigateTo(0);
+ return;
+ }
let parent = term.parent;
this.navigateTo(parseInt(parent));
}
@@ -419,11 +458,17 @@
addTermToModal(termId) {
const term = this.store.get(termId);
if (!term) return;
+ const field = this.currentField();
+ if (!field) return;
+ if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
const item = window.getTemplate('selectedTerm');
+ if (!item) return;
+
item.dataset.id = termId;
- item.querySelector('span').textContent = term.path;
- item.querySelector('button').title = `Remove ${name}`;
+ item.dataset.taxonomy = field.taxonomy;
+ item.querySelector('.item-name').textContent = term.path;
+ item.querySelector('button').title = `Remove ${term.name}`;
this.ui.selected.append(item);
}
@@ -461,10 +506,18 @@
let selectors = this.selectors.field;
let button = element.querySelector('button.taxonomy-toggle');
- if (options.size === 0){
+
+ if (Object.keys(options).length === 0){
if (!button) return;
- options = button.dataset;
- if (options.size === 0) return;
+ options = {
+ taxonomy: button.dataset.taxonomy,
+ single: button.dataset.single,
+ plural: button.dataset.plural,
+ search: Object.hasOwn(button.dataset, 'search'),
+ 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;
@@ -478,21 +531,33 @@
singular: options.single??'',
plural: options.plural??'',
name: element.dataset.field,
- canSearch: Object.hasOwn(options, 'search'),
+ canSearch: options.search??false,
limit: options.limit??0,
- hasAutocomplete: Object.hasOwn(options, 'autocomplete'),
- canCreate: Object.hasOwn(options, 'creatable'),
- isRequired: Object.hasOwn(options, 'required'),
+ hasAutocomplete: options.autocomplete??false,
+ canCreate: options.creatable??false,
+ isRequired: options.required??false,
toggle: button,
+ create: {
+ button: null,
+ span: null
+ },
selectors: selectors,
ui: window.uiFromSelectors(selectors, element),
checked: false,
};
- if (!config.taxonomy) return;
+ if (!config.taxonomy) {
+ console.error('TaxonomySelector: Field missing taxonomy', element);
+ return;
+ }
+ if (!config.singular || !config.plural) {
+ console.warn('TaxonomySelector: Field missing singular/plural labels', element);
+ config.singular = config.taxonomy.replace('jvb_', '');
+ config.plural = config.singular + 's';
+ }
this.fields.set(fieldId, config);
//Check for stored selected terms in hidden input
- this.setSelectedFromValue(input);
+ this.setSelectedFromValue(fieldId, input);
if (this.isInitializing) {
@@ -504,8 +569,13 @@
}
setSelectedFromValue(fieldId, input) {
+ if (!input) return;
+ if (!fieldId) return;
+ let field = this.fields.get(fieldId);
+ if (!field) return;
+
let selected = new Set();
- input.value.value.trim()
+ input.value.trim()
.split(',')
.map(id => parseInt(id.trim()))
.filter(id => !isNaN(id))
@@ -524,8 +594,10 @@
if (field.limit !== 0 && selected.size >= field.limit) return;
selected.add(parseInt(termId));
+ if (!this.container.open) {
+ this.updateFieldValue(fieldId);
+ }
this.addTermToDisplay(termId, fieldId);
- this.updateFieldValue(fieldId);
this.checkLimits(fieldId);
}
removeSelected(termId, fieldId = null) {
@@ -535,20 +607,27 @@
if (!field || !term) return;
this.selectedTerms.get(fieldId).delete(parseInt(termId));
- const selectedItem = field.ui.selected.querySelector(`[data-i"${termId}"]`);
+ const selectedItem = field.ui.selected.querySelector(`[data-id="${termId}"]`);
if (selectedItem) selectedItem.remove();
if (this.container.open) {
let item = this.ui.selected.querySelector(`[data-id="${termId}"]`);
if (item) item.remove();
+ let checkbox = this.ui.terms.list.querySelector(`[type=checkbox][data-id="${termId}"]`);
+ if (checkbox) {
+ checkbox.checked = false;
+ }
}
- this.updateFieldValue(fieldId);
+ if (!this.container.open) {
+ 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(',');
+ field.ui.value.value = selected.join(',');
}
checkLimits(fieldId) {
@@ -561,8 +640,9 @@
updateFieldFromInput(input) {
const fieldId = this.getFieldId(input);
+ if (!fieldId) return;
const field = this.fields.get(fieldId);
- if (!fieldId || !field) return;
+ if(!field) return;
this.setSelectedFromValue(fieldId, input);
this.updateFieldUI(fieldId);
@@ -570,7 +650,7 @@
updateFieldUI(fieldId) {
const field = this.fields.get(fieldId);
- let selected = this.selectedTerms.get(fieldId);
+ let selected = this.selectedTerms.get(fieldId)??new Set();
if (!field || selected.size === 0) return;
Array.from(selected).forEach(termId => {
@@ -594,11 +674,28 @@
});
}
- showModalTerms(append = true, showPath = false) {
+ showModalTerms(showPath = false) {
+ const field = this.currentField();
const terms = this.store.getFiltered();
- if (terms.size === 0) return;
- if (!append) {
- window.removeChildren(this.ui.terms.list);
+ if (terms.length === 0) {
+ if (this.store.filters.page??1 === 1) {
+ window.removeChildren(this.ui.terms.list);
+ }
+ this.setMessage(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);
+ }
+ }
+
+ if (this.ui.terms.sentinel) {
+ if (this.store.lastResponse?.has_more) {
+ this.observer.observe(this.ui.terms.sentinel);
+ } else {
+ this.observer.unobserve(this.ui.terms.sentinel);
+ }
}
const currentParent = this.store.filters.parent??0;
@@ -611,10 +708,12 @@
... term
});
if (element) {
- fragment.appendChild(element);
+ fragment.append(element);
}
});
+ this.setMessage(false);
+
this.ui.terms.list.append(fragment);
}
createTermElement(term) {
@@ -638,6 +737,7 @@
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,
@@ -648,8 +748,9 @@
label.dataset.path,
nameSpan.textContent
] = [
+ term.id,
`${field.element.id}-${term.id}`,
- `${field.container.id}-${field.taxonomy}-select`,
+ `${field.element.id}-${field.taxonomy}-select`,
term.id,
!isSelected && limitReached,
isSelected,
@@ -673,30 +774,28 @@
showAutocompleteTerms() {
const field = this.currentField();
const terms = this.currentTerms();
- if (!field || terms.size ===0) return;
+ if (!field) return;
- const dropdown = field.ui.dropdown;
+ const dropdown = field.ui.dropdown.list;
+ if (!dropdown) return;
+
window.removeChildren(dropdown);
if (terms.length === 0) {
- this.showEmptyState(`No ${field.plural} found.`, dropdown);
+ this.setMessage(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);
- const query = field.ui.search?.value;
- if (field.canCreate && query.length >= 2 && this.creator) {
- const createButton = this.createTermButton(query);
- if (createButton) {
- dropdown.append(createButton);
- }
+ if (field.ui.dropdown?.wrapper) {
+ field.ui.dropdown.wrapper.hidden = false;
}
-
- dropdown.hidden = false;
}
createAutocompleteTerm(term) {
const item = window.getTemplate('autocompleteItem');
@@ -732,15 +831,6 @@
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;
@@ -826,11 +916,16 @@
handleStoreEvent(event, data) {
const handlers = {
'data-loaded': () => this.handleDataLoaded(),
- 'filters-changed': () => this.handleFiltersChanged(),
+ 'filters-changed': () => this.handleFiltersChanged(data),
'fetch-error': () => this.handleFetchError()
};
- handlers[event]?.();
+ try {
+ 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;
@@ -845,63 +940,52 @@
}
if (this.activeField) {
this.showResults(true);
+ return;
}
+ this.setMessage(false);
}
showResults(isAutoComplete = false) {
- this.setLoading(false);
+ this.setMessage(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()
- }
+ if (isAutoComplete) {
+ this.showAutocompleteTerms();
+ } else {
+ this.showModalTerms(isSearch);
}
- this.a11y.announce(terms.length, append);
+
+ this.a11y.announce(terms.length);
}
- handleFiltersChanged() {
- // if (this.modal?.open) {
- // this.setLoading();
- // }
+ handleFiltersChanged(data) {
+ //maybe do something?
}
handleFetchError(error) {
- this.setLoading(false);
+ const field = this.currentField();
+ const message = field
+ ? `Failed to load ${field.plural}`
+ : 'Failed to load data';
+
+ this.setMessage(true, message, false);
+ console.error('Store fetch error:', error);
}
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,
@@ -914,55 +998,110 @@
}
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);
+ setCreateButton(show = true) {
+ const field = this.currentField();
+ if (!field || !field.canCreate || !this.creator) return;
- 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';
+ const conf = (this.container.open) ? this.ui : field.ui;
- 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;
- }
+ if (!conf.create?.button || !conf.create?.span) return;
+
+ const createButton = conf.create.button;
+ const buttonSpan = conf.create.span;
+ const input = (this.container.open) ? conf.search.input : conf.search;
+ if (!input) return;
+
+ let results = this.currentTerms()??[];
+ let matches = results.map(t => t.name);
+
+ let query = input.value;
+ const willShow = show && query.length >= 2 && !matches.includes(query);
+ createButton.hidden = !willShow;
+ if (willShow) {
+ buttonSpan.textContent = input.value??'';
}
}
- 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;
+ async maybeCreateTerm(e) {
+ const field = this.currentField();
+ if (!field) return;
+
+ window.debouncer.cancel(`${field.id}-search-results`);
+ let data = {
+ taxonomy: field.taxonomy,
+ parent: this.store.filters.parent??0
}
- container.append(emptyElement);
+ //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);
+ 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) {
+ this.addSelected(term.id, field.id);
+ }
+ 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;
+ 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;
+
+ p.hidden = !show;
+ if (show) {
+ if (message && pText) {
+ if (type && window.typeLoop && pText) {
+ if (this.messageText[field.id]) {
+ this.messageText[field.id]();
+ delete this.messageText[field.id];
+ }
+ this.messageText[field.id] = window.typeLoop(pText, message);
+ } else {
+ pText.textContent = message;
+ }
+
+ }
+ } else {
+ if (this.messageText[field.id]) {
+ this.messageText[field.id]();
+ delete this.messageText[field.id];
+ }
+ }
}
/**************************************************
SUBSCRIBERS
@@ -984,16 +1123,49 @@
CLEANUP
******************************************************/
destroy() {
+ // Cancel all debounced operations for this instance
+ this.fields.forEach((field, fieldId) => {
+ window.debouncer.cancel(`${fieldId}-search`);
+ window.debouncer.cancel(`${fieldId}-search-results`);
+ });
+
+ // Stop any typeLoop animations
+ Object.keys(this.messageText).forEach(key => {
+ if (this.messageText[key]) {
+ this.messageText[key]();
+ }
+ });
+ this.messageText = {};
+
+ // Disconnect observer
+ if (this.ui.terms?.sentinel) {
+ this.observer?.unobserve(this.ui.terms.sentinel);
+ }
+ this.observer?.disconnect();
+
+ // Remove event listeners
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);
+ document.removeEventListener('focus', this.focusHandler, true);
+ document.removeEventListener('blur', this.blurHandler, true);
- this.observer?.disconnect();
+ // Clear data structures
this.subscribers.clear();
this.fields.clear();
this.selectedTerms.clear();
+ this.batchFetch.clear();
+
+ // Cleanup creator if exists
+ if (this.creator) {
+ this.creator.destroy();
+ this.creator = null;
+ }
+
+ // Unsubscribe from store
+ if (this.store) {
+ this.store = null;
+ }
}
}
@@ -1004,3 +1176,4 @@
}
});
});
+
--
Gitblit v1.10.0