Jake Vanderwerf
2026-01-11 181938ea32cf2c11d10d56018df22586adff7860
assets/js/concise/TaxonomySelector.js
@@ -1,3 +1,4 @@
class TaxonomySelector {
   constructor() {
      this.container = document.querySelector('dialog#jvb-selector');
@@ -67,7 +68,7 @@
   initElements() {
      this.selectors = {
         search: {
            input: '[type=search]',
            input: '[type="search"]',
            clear: '.clear-search',
            container: '.search-wrapper',
            results: '.search-results',
@@ -99,7 +100,7 @@
         },
         favourites: '.favourite-terms',
         field: {
            toggle: 'button.taxonomy-toggle',
            toggle: 'button.taxonomy-toggle, [data-filter="taxonomy"]',
            value: 'input[type="hidden"]',
            selected: '.selected-items',
            dropdown: {
@@ -147,18 +148,16 @@
   }
   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 = '';
         }
@@ -244,7 +243,7 @@
      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();
@@ -262,7 +261,7 @@
      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();
@@ -334,6 +333,7 @@
      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);
   }
@@ -372,7 +372,9 @@
      );
      this.modal.subscribe((event, data) => {
         switch (event) {
            case 'modal-close':
               this.closeModal()
               break;
         }
      });
   }
@@ -393,7 +395,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 +404,81 @@
      }
      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;
@@ -493,7 +553,7 @@
   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;
      }
@@ -505,7 +565,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.taxonomy-toggle');
      if (Object.keys(options).length === 0){
         if (!button) return;
@@ -536,6 +597,7 @@
         hasAutocomplete: options.autocomplete??false,
         canCreate: options.creatable??false,
         isRequired: options.required??false,
         isFilter: isFilter,
         toggle: button,
         create: {
            button: null,
@@ -545,6 +607,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;
@@ -569,17 +635,19 @@
   }
   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 +662,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 +675,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);
      }
@@ -633,7 +701,7 @@
   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 +719,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);
@@ -665,8 +733,9 @@
         .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}`;
@@ -690,6 +759,8 @@
         }
      }
      this.setCreateButton(true);
      if (this.ui.terms.sentinel) {
         if (this.store.lastResponse?.has_more) {
            this.observer.observe(this.ui.terms.sentinel);
@@ -712,7 +783,9 @@
         }
      });
      this.setMessage(false);
      if (terms.length > 0) {
         this.setMessage(false);
      }
      this.ui.terms.list.append(fragment);
   }
@@ -793,7 +866,7 @@
      }
      this.setCreateButton(true);
      if (field.ui.dropdown?.wrapper) {
      if (field.ui.dropdown.wrapper) {
         field.ui.dropdown.wrapper.hidden = false;
      }
   }
@@ -803,6 +876,7 @@
      item.dataset.id = term.id;
      item.textContent = term.path || term.name;
      return item;
   }
   /******************************************************************
@@ -813,7 +887,7 @@
      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;
@@ -823,7 +897,9 @@
      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);
@@ -929,7 +1005,9 @@
   }
   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));
      }
@@ -1014,7 +1092,6 @@
      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;
@@ -1076,9 +1153,12 @@
   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;
@@ -1176,4 +1256,3 @@
      }
   });
});