| | |
| | | |
| | | class TaxonomySelector { |
| | | constructor() { |
| | | this.container = document.querySelector('dialog#jvb-selector'); |
| | |
| | | initElements() { |
| | | this.selectors = { |
| | | search: { |
| | | input: '[type=search]', |
| | | input: '[type="search"]', |
| | | clear: '.clear-search', |
| | | container: '.search-wrapper', |
| | | results: '.search-results', |
| | |
| | | }, |
| | | favourites: '.favourite-terms', |
| | | field: { |
| | | toggle: 'button.taxonomy-toggle', |
| | | toggle: 'button.taxonomy-toggle, [data-filter="taxonomy"]', |
| | | value: 'input[type="hidden"]', |
| | | selected: '.selected-items', |
| | | dropdown: { |
| | |
| | | } |
| | | |
| | | handleClick(e) { |
| | | const fieldId = (this.container.open) ? this.activeField : this.getFieldId(e.target); |
| | | 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]'); |
| | | const autoComplete = window.targetCheck(e, '.item.autocomplete'); |
| | | |
| | | if (autoComplete) { |
| | | let termId = parseInt(autoComplete.dataset.id); |
| | | this.addSelected(termId, fieldId); |
| | | if (field.ui.dropdown.wrapper) { |
| | | field.ui.dropdown.wrapper.hidden = true; |
| | | } |
| | | |
| | | this.scheduleHideDropdown(fieldId); |
| | | if (field.ui.search) { |
| | | field.ui.search.value = ''; |
| | | } |
| | |
| | | if (!this.container.contains(e.target)) { |
| | | return; |
| | | } |
| | | if (e.target.type !== 'checkbox') return; |
| | | if (!['checkbox', 'button'].includes(e.target.type)) return; |
| | | e.preventDefault(); |
| | | e.stopPropagation(); |
| | | |
| | |
| | | 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(); |
| | |
| | | const field = this.fields.get(fieldId); |
| | | if (!fieldId || ! field) return; |
| | | if (!field.hasAutocomplete || this.container.open) return; |
| | | if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return; |
| | | |
| | | this.scheduleHideDropdown(fieldId); |
| | | } |
| | |
| | | ); |
| | | this.modal.subscribe((event, data) => { |
| | | switch (event) { |
| | | |
| | | case 'modal-close': |
| | | this.closeModal() |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | 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.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; |
| | | |
| | |
| | | |
| | | registerField(element, options = {}) { |
| | | let input = element.querySelector('input[type="hidden"]'); |
| | | if (!input) { |
| | | if (!input && !Object.hasOwn(element.dataset, 'filter')) { |
| | | console.warn('TaxonomySelector: No hidden input found for field', element); |
| | | return; |
| | | } |
| | |
| | | |
| | | |
| | | 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.taxonomy-toggle'); |
| | | |
| | | if (Object.keys(options).length === 0){ |
| | | if (!button) return; |
| | |
| | | hasAutocomplete: options.autocomplete??false, |
| | | canCreate: options.creatable??false, |
| | | isRequired: options.required??false, |
| | | isFilter: isFilter, |
| | | toggle: button, |
| | | create: { |
| | | button: null, |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | |
| | | .some(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}`; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | this.setCreateButton(true); |
| | | |
| | | if (this.ui.terms.sentinel) { |
| | | if (this.store.lastResponse?.has_more) { |
| | | this.observer.observe(this.ui.terms.sentinel); |
| | |
| | | } |
| | | }); |
| | | |
| | | this.setMessage(false); |
| | | if (terms.length > 0) { |
| | | this.setMessage(false); |
| | | } |
| | | |
| | | this.ui.terms.list.append(fragment); |
| | | } |
| | |
| | | } |
| | | this.setCreateButton(true); |
| | | |
| | | if (field.ui.dropdown?.wrapper) { |
| | | if (field.ui.dropdown.wrapper) { |
| | | field.ui.dropdown.wrapper.hidden = false; |
| | | } |
| | | } |
| | |
| | | |
| | | item.dataset.id = term.id; |
| | | item.textContent = term.path || term.name; |
| | | |
| | | return item; |
| | | } |
| | | /****************************************************************** |
| | |
| | | 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.querySelector('.item-name').textContent = term.path; |
| | | item.querySelector('button').title = `Remove ${term.name}`; |
| | | |
| | | field.ui.selected.append(item); |
| | | if (field.ui.selected) { |
| | | field.ui.selected.append(item); |
| | | } |
| | | |
| | | if (this.container.open) { |
| | | this.addTermToModal(termId); |
| | |
| | | } |
| | | handleDataLoaded() { |
| | | const taxonomy = this.store.filters.taxonomy; |
| | | if (taxonomy?.includes(',')) { |
| | | |
| | | // Always update fields for loaded taxonomies (handles both single and batch) |
| | | if (taxonomy) { |
| | | const taxonomies = taxonomy.split(',').map(t => t.trim()); |
| | | taxonomies.forEach(tax => this.updateFieldsForTaxonomy(tax)); |
| | | } |
| | |
| | | if (!field || !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; |
| | |
| | | setMessage(show = true, message = '', type = true) { |
| | | const field = this.currentField(); |
| | | if (!field) return; |
| | | |
| | | 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; |
| | | |
| | |
| | | } |
| | | }); |
| | | }); |
| | | |