Jake Vanderwerf
2026-09-15 01e9a8e40351a87f6548a4f55de842d6f6918ad0
assets/js/concise/FormController.js
@@ -10,6 +10,7 @@
      this.inputs = new Map();
      this.repeaters = new Map();
      this.tagLists = new Map();
      this.locations = new Map();
      this.charLimits = new Map();
      this.quantityFields = new Map();
      this.quillInstances = new Map(); // formId -> Set of quill instances
@@ -23,6 +24,7 @@
      this.summaryTemplate = false;
      this.init();
   }
   init() {
      this.templates = window.jvbTemplates;
@@ -104,6 +106,14 @@
            inputs: this.inputSelectors,           //querySelectorAll
            value: 'input[type="hidden"]'    //querySelectorAll
         },
         location: {
            location: '[data-field-type="location"]', //querySelectorAll
            search: '[data-location-search]',
            results: '.search-results',
            message: '.message',
            preview: '.location-preview',
            fields: 'fieldset.location input',
         },
         tag: {
            label: '.tag-label'
         },
@@ -418,6 +428,7 @@
      }
      let form = this.getForm(e.target);
      console.log(this.getFieldValue(e.target), 'value');
      this.updateItem(field.dataset.field, this.getFieldValue(e.target), form);
   }
@@ -506,6 +517,9 @@
    * @param form
    */
   updateItem(name, value, form) {
      console.log('Updating field');
      console.log(name,'name');
      console.log(value,'value');
      if (value === undefined) return;
      if (!this.changes.has(form.id)) {
         this.changes.set(form.id, {
@@ -853,7 +867,8 @@
         repeater: this.repeaters,
         tagList: this.tagLists,
         charLimit: this.charLimits,
         quantity: this.quantityFields
         quantity: this.quantityFields,
         location: this.locations
      };
      for (let [type, check] of Object.entries(checks)) {
         if (check.size === 0) continue;
@@ -873,6 +888,9 @@
                  case 'quantity':
                     this.removeQuantityListeners(item.element);
                     break;
                  case 'location':
                     this.removeLocationListeners(item.element);
                     break;
               }
               if (check.has(item.id)) {
@@ -963,6 +981,7 @@
   initializeFields(container, config = null) {
      const fieldHandlers = {
         '[data-editor]': () => this.checkForQuill(container,config),
         '[data-field-type="location"]': () => this.checkForLocationFields(container, config),
         'div.quantity': () => this.checkForQuantity(container),
         '.repeater': () => this.checkForRepeaters(container, config),
         '.field.tag-list': () => this.checkForTagLists(container),
@@ -1336,6 +1355,127 @@
   }
   checkForLocationFields(form, config) {
      form.querySelectorAll(this.selectors.location.location).forEach(field => {
         let ui = window.uiFromSelectors(this.selectors.location, field);
         if (!ui.search) return;
         ui.fields = {};
         field.querySelectorAll(this.selectors.location.fields).forEach(f => {
            ui.fields[f.dataset.fieldName] = f;
         });
         let id = window.generateID('location');
         field.dataset.locationId = id;
         this.locations.set(id, {
            id: id,
            form: config?.id,
            element: field,
            ui: ui,
            results: []
         });
         this.addLocationListeners(field);
      });
   }
      addLocationListeners(el) {
         el.addEventListener('input', this.locationInputHandler ??= this.handleLocationInput.bind(this));
         el.addEventListener('click', this.locationClickHandler ??= this.handleLocationClick.bind(this));
      }
      removeLocationListeners(el) {
         el.removeEventListener('input', this.locationInputHandler);
         el.removeEventListener('click', this.locationClickHandler);
      }
      handleLocationInput(e) {
         if (!e.target.matches(this.selectors.location.search)) return;
         let field = e.target.closest('[data-location-id]');
         if (!field) return;
         let conf = this.locations.get(field.dataset.locationId);
         if (!conf) return;
         let query = e.target.value.trim();
         if (query.length < 3) {
            conf.ui.results.replaceChildren();
            return;
         }
         window.debouncer.schedule(
            `location:${conf.id}`,
            () => this.searchLocation(conf, query),
            400
         );
      }
      async searchLocation(conf, query) {
         conf.ui.message.hidden = false;
         try {
            let response = await window.auth.fetch(
               `${jvbSettings.api}geocode?q=${encodeURIComponent(query)}`,
               { method: 'GET' }
            );
            conf.results = response.ok ? await response.json() : [];
         } catch (err) {
            console.error('Geocode error:', err);
            conf.results = [];
         }
         conf.ui.message.hidden = true;
         this.renderLocationResults(conf);
      }
      renderLocationResults(conf) {
         conf.ui.results.replaceChildren();
         if (conf.results.length === 0) {
            conf.ui.results.innerHTML = '<li>No results found</li>';
            return;
         }
         conf.results.forEach((result, index) => {
            let li = document.createElement('li');
            li.className = 'autocomplete item btn';
            li.dataset.resultIndex = index;
            li.textContent = result.display_name;
            conf.ui.results.append(li);
         });
      }
      handleLocationClick(e) {
         let item = e.target.closest('[data-result-index]');
         if (!item) return;
         let field = e.target.closest('[data-location-id]');
         if (!field) return;
         let conf = this.locations.get(field.dataset.locationId);
         if (!conf) return;
         let result = conf.results[item.dataset.resultIndex];
         if (!result) return;
         this.populateLocationFromResult(conf, result);
         conf.ui.results.replaceChildren();
         conf.ui.search.value = result.display_name;
         let form = this.getForm(field);
         if (form) this.updateItem(field.dataset.field, this.getLocationValue(field), form);
      }
      populateLocationFromResult(conf, result) {
         const addr = result.address ?? {};
         const map = {
            address: addr.house_number || '',
            lat: result.lat,
            lng: result.lon,
            street: addr.road || '',
            city: addr.city || addr.town || addr.village || '',
            province: addr.state || '',
            postal_code: addr.postcode || '',
            country: addr.country || '',
         };
         for (let [field, input] of Object.entries(conf.ui.fields)) {
            input.value = map[field]??'';
         }
         if (conf.ui.preview) {
            conf.ui.preview.dataset.lat = map.lat;
            conf.ui.preview.dataset.lng = map.lng;
         }
      }
   checkForConditionalFields(form) {
      form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
         const dependsOn = field.dataset.dependsOn;
@@ -1892,6 +2032,7 @@
      let conf = this.getItem(element);
      let fieldName = conf.field?.dataset.field??false;
      if (!fieldName) return false;
      switch (type) {
@@ -1906,7 +2047,7 @@
         //Do we actually need anything here? I think each subfield just
         case 'location':
            return this.getLocationValue(element, conf);
            return this.getLocationValue(element);
         case 'selector':
         case 'upload':
@@ -2015,15 +2156,15 @@
      });
      return value;
   }
   getLocationValue(element, conf) {
      if(!conf.values){
         conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
         this.saveItem(conf);
      }
   getLocationValue(element) {
      let field = this.getField(element);
      let conf = this.locations.get(field.dataset.locationId);
      if (!conf) return;
      let value = {};
      conf.values.forEach(input => {
         value[input.dataset.locationField] = input.value;
      });
      for (let [field, input] of Object.entries(conf.ui.fields)) {
         value[field] = input.value;
      }
      return value;
   }
   getHiddenInputValue(element, conf, fieldName) {
@@ -2356,10 +2497,13 @@
   }
}
document.addEventListener('DOMContentLoaded', async function () {
   window.auth.subscribe(event => {
      if (event === 'auth-loaded') {
         window.jvbForm = new FormController();
         document.dispatchEvent(new CustomEvent('jvbFormLoaded'));
         document.querySelectorAll('form[data-auto]').forEach(form => {
            window.jvbForm.registerForm(form, {});
         });