| | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | 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', |
| | |
| | | child: '.toggle-children', |
| | | pathLevel: '.path-level', |
| | | }, |
| | | loading: { |
| | | loading: '.loading', |
| | | text: '.loading span', |
| | | message: { |
| | | message: 'p.message', |
| | | text: 'p.message span', |
| | | }, |
| | | selected: '.selected-items', |
| | | modal: { |
| | |
| | | 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() { |
| | |
| | | } |
| | | |
| | | 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; |
| | | |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | |
| | | 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); |
| | |
| | | } |
| | | } |
| | | |
| | | 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 (!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); |
| | |
| | | window.debouncer.cancel(`${fieldId}-search-results`); |
| | | |
| | | if (!this.container.open){ |
| | | this.activeField = fieldId; |
| | | this.preloadTaxonomy(field.taxonomy); |
| | | this.setField(fieldId); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | |
| | | 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 |
| | | ); |
| | |
| | | this.container, |
| | | { |
| | | handleForm: false, |
| | | save: null, |
| | | open: null |
| | | } |
| | | ); |
| | |
| | | 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); |
| | | } |
| | |
| | | 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)); |
| | | } |
| | |
| | | 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); |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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)) |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | |
| | | 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); |
| | |
| | | |
| | | 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 => { |
| | |
| | | }); |
| | | } |
| | | |
| | | 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; |
| | |
| | | ... term |
| | | }); |
| | | if (element) { |
| | | fragment.appendChild(element); |
| | | fragment.append(element); |
| | | } |
| | | }); |
| | | |
| | | this.setMessage(false); |
| | | |
| | | this.ui.terms.list.append(fragment); |
| | | } |
| | | createTermElement(term) { |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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'); |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | } |
| | | 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, |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | }); |
| | | }); |
| | | |