Jake Vanderwerf
2026-02-17 a24a06002081ad71a78ffeff9072725ba39cf121
assets/js/concise/FormController.js
@@ -1,983 +1,1385 @@
class FormController {
   constructor(config = {}) {
      this.config = {
         collectFormData: false,
         ... config
      }
   constructor() {
      this.a11y = window.jvbA11y;
      this.error = window.jvbError;
      this.queue = window.jvbQueue;
      this.populate = window.jvbPopulate;
      this.changes = new Map();
      this.forms = new Map();
      this.inputs = new Map();
      this.repeaters = new Map();
      this.tagLists = new Map();
      this.charLimits = new Map();
      this.quantityFields = new Map();
      this.quillInstances = new Map(); // formId -> Set of quill instances
      this.dependencies = new Map();
      this.subscribers = new Set();
      this.isRestoring = false;
      this.hasListeners = false;
      this.summaryTemplate = false;
      this.init();
   }
   init() {
      this.templates = window.jvbTemplates;
      this.defineSummaryTemplate();
      this.initElements();
      this.initListeners();
      this.initStore();
      this.initValidators();
   }
   initElements() {
      this.inputSelectors = 'input, textarea, select';
      this.selectors = {
         tabs: {
            nav: 'nav.tabs',
            sections: '.tab.content',        //querySelectorAll
            progress: {
               progress: '.progress',
               fill: '.progress .fill',
               details: '.progress .details',
               icon: '.progress .icon'
            },
            buttons: 'nav.tabs button',
         },
         dependsOn: '[data-depends-on]',
         forms: {
            status: {
               status: '.fstatus',
               message: '.fstatus .message',
               icon: '.fstatus .icon',
               actions: '.fstatus .actions'
            }
         },
         inputs: this.inputSelectors,              //querySelectorAll
         fields: {
            field: '.field',              //querySelectorAll
            label: 'label',
            success: '.success',
            error: '.error',
            message: '.validation-message',
         },
         repeater: {
            repeater: '.repeater',           //querySelectorAll
            header: '.repeater-row-header',
            remove: '.remove-row',
            add: '.add-repeater-row',
            template: 'template',
            items: '.repeater-items',
            inputs: this.inputSelectors               //querySelectorAll
         },
         tagList: {
            tagList: '.field.tag-list',         //querySelectorAll
            input: '.row',
            add: '.add-tag',
            remove: '.remove-tag',
            label: '.tag-label',
            items: '.tag-items',
            item: '.tag-item',
            inputs: this.inputSelectors,           //querySelectorAll
            value: 'input[type="hidden"]'    //querySelectorAll
         },
         tag: {
            label: '.tag-label'
         },
         number: {
            number: '.field div.quantity',
            increase: 'button.increase',
            decrease: 'button.decrease',
            input: 'input[type="number"]'
         },
         limits: {
            hasLimit: '[data-maxlength]',
            limit: '.limit',
            current: '.current',
         }
      };
   }
   initListeners() {
      this.clickHandler = this.handleClick.bind(this);
      this.changeHandler = this.handleChange.bind(this);
      this.blurHandler =  this.handleBlur.bind(this);
      this.inputHandler = this.handleInput.bind(this);
      this.submitHandler = this.handleSubmit.bind(this);
      this.quantityClick = this.handleQuantityClick.bind(this);
      this.repeaterClick = this.handleRepeaterClick.bind(this);
      this.tagListClick = this.handleTagListClick.bind(this);
      this.tagListInput = this.handleTagListInput.bind(this);
   }
      addFormListeners(form) {
         form.addEventListener('click', this.clickHandler);
         form.addEventListener('change', this.changeHandler);
         form.addEventListener('input', this.inputHandler);
         form.addEventListener('blur', this.blurHandler);
         form.addEventListener('submit', this.submitHandler);
      }
      removeFormListeners(form) {
         form.removeEventListener('click', this.clickHandler);
         form.removeEventListener('change', this.changeHandler);
         form.removeEventListener('input', this.inputHandler);
         form.removeEventListener('blur', this.blurHandler);
         form.removeEventListener('submit', this.submitHandler);
      }
   initStore() {
      const store = window.jvbStore.register(
         'forms',
         {
            storeName: 'forms',
            keyPath: 'formId',
            keyPath: 'id',
            indexes: [
               { name: 'status', keyPath: 'status' },
               { name: 'operationId', keyPath: 'operationId' },
               { name: 'src', keyPath: 'src'},
               { name: 'timestamp', keyPath: 'timestamp' },
               { name: 'formType', keyPath: 'type' }
            ],
            TTL: 7 * 24 * 60 * 1000, //7 days
            validateData: true,
            delayFetch: true
         });
      this.store = store.forms;
      this.debouncer = window.debouncer;
      this.store.subscribe((event, data)=> {
         if (event === 'data-ready') {
            let stored = this.store.getFiltered();
      this.ignore = [];
      this.populateForm = window.jvbPopulate;
      this.subscribers = new Set();
      this.forms = new Map();
      this.specialFields = new Map();
      this.dependencies = new Map();
      // Validation (YOU ARE GREAT!)
      this.validators = this.initValidators();
      this.touchedFields = new Set();
      // Auto-save configuration
      this.autoSaveDefaults = {
         delay: 3000, // 3 seconds
         typingDelay: 1500, // 1.5 seconds for text fields
         enabled: true
      };
      // Repeater field management
      this.activeRepeaters = new Map();
      this.repeaterDelays = {
         change: 6000,
         typing: 3000,
         blur: 1500,
         add: 500,
         remove: 800,
         reorder: 1000
      };
      this.isTimeline = window.crudManager && window.crudManager.isTimeline;
      // Bind handlers
      this.clickHandler = this.handleClick.bind(this);
      this.changeHandler = this.handleChange.bind(this);
      this.submitHandler = this.handleSubmit.bind(this);
      this.inputHandler = this.handleInput.bind(this);
      this.blurHandler = this.handleBlur.bind(this);
      //Processors
      this.processRepeaterField = this.processRepeaterField.bind(this);
      this.processGroupField = this.processGroupField.bind(this);
      this.processLocationField = this.processLocationField.bind(this);
      this.processRegularField = this.processRegularField.bind(this);
      this.init();
   }
   async init() {
      this.store.subscribe(this.handleStoreEvent.bind(this));
      // Set up global form handlers for standalone forms
      this.initListeners();
      if (window.jvbQueue) {
         window.jvbQueue.subscribe((event, data) => {
            if (event === 'operation-completed' && data.type === 'form') {
               this.handleOperationComplete(data);
            let pending = stored.filter(form=> form.src ===  window.location.pathname);
            for (let form of pending) {
               this.showPendingNotification(form.id, form.changes);
            }
         });
      }
   }
   /**
    * Handle operation completion - clear related form cache
    */
   async handleOperationComplete(operation) {
      // Clear the form data from store
      if (operation.formId) {
         try {
            await this.store.delete(operation.formId);
         } catch (error) {
            console.warn('Failed to clear form cache:', error);
         } else if (event === 'operation-status' && data.status === 'completed') {
            if (data.config) {
               this.store.delete(data.config.id);
            }
         }
      }
      // Clear any related form state
      const form = this.forms.get(operation.formId);
      if (form) {
         form.isDirty = false;
         form.lastSaved = Date.now();
         form.data = {};
      }
   }
   handleStoreEvent(event, data) {
      switch(event) {
         case 'item-saved':
            if (data.item.status === 'autosave') {
               // this.showFormStatus(data.item.formId, 'autosave');
            }
            break;
         case 'data-loaded':
            this.checkPendingForms();
            break;
      }
   }
   /**
    * Check for pending forms from current page
    */
   async checkPendingForms() {
      const allForms = await this.store.getAll();
      const currentPath = window.location.pathname;
      const pendingForms = allForms.filter(form => {
         if (form.status !== 'draft') return false;
         // Check if form is from current page
         const formPath = form.data?._wp_http_referer;
         return formPath === currentPath;
      });
      pendingForms.forEach(item => {
         const formElement = this.findFormElement(item);
         if (!formElement) return;
         // Register form if not already registered
         let formConfig = this.forms.get(item.formId);
         if (!formElement.dataset.formId) {
            formConfig = this.registerForm(formElement);
   }
      showPendingNotification(formId, changes) {
         let form = this.forms.get(formId);
         if (!form) return;
         let element = form.element;
         if (!element) {
            console.warn(`Form element not found for: ${formId}`);
            return;
         }
         // Set flag to prevent event handlers from firing
         this.isRestoring = true;
         // Auto-populate the form
         new this.populateForm(formElement, item.data);
         const notification = document.createElement('div');
         notification.className = 'pendingChanges';
         notification.innerHTML = `
         <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
        <button class="restore" type="button" data-form-id="${formId}">Restore</button>
        <button class="discard" type="button" data-form-id="${formId}">Discard</button>`;
         // Reset flag after a tick (gives DOM time to settle)
         setTimeout(() => {
         element.insertBefore(notification, form.ui.status.status);
         notification.querySelector('.restore').addEventListener('click', async () => {
            this.isRestoring = true;
            let theChanges = {['fields']: changes};
            this.populate.populate(element, theChanges);
            this.a11y.announce('Previous changes restored');
            this.isRestoring = false;
         }, 0);
            notification.remove();
         });
         // Show restore status
         this.showFormStatus(item.formId, 'restored');
         notification.querySelector('.discard').addEventListener('click', async () => {
            await this.store.delete(formId);
            this.a11y.announce('Previous changes discarded');
            notification.remove();
         });
         if (window.jvbA11y) {
            window.jvbA11y.announce('Your previous entry has been restored');
      }
   initValidators() {
      this.validators = {
         email: {
            pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: 'Please enter a valid email address'
         },
         url: {
            pattern: /^https?:\/\/.+\..+/,
            message: 'Please enter a valid URL starting with https://'
         },
         phone: {
            pattern: /^[\d\s\-+().]+$/,
            message: 'Please enter a valid phone number'
         },
         number: {
            test: (value, fieldWrapper) => {
               const num = parseFloat(value);
               if (isNaN(num)) return 'Please enter a valid number';
               const min = fieldWrapper.dataset.min;
               const max = fieldWrapper.dataset.max;
               if (min !== undefined && num < parseFloat(min)) {
                  return `Value must be at least ${min}`;
               }
               if (max !== undefined && num > parseFloat(max)) {
                  return `Value must be at most ${max}`;
               }
               return true;
            }
         },
         text: {
            test: (value, fieldWrapper) => {
               const minLength = fieldWrapper.dataset.minlength;
               const maxLength = fieldWrapper.dataset.maxlength;
               if (minLength && value.length < parseInt(minLength)) {
                  return `Must be at least ${minLength} characters`;
               }
               if (maxLength && value.length > parseInt(maxLength)) {
                  return `Must be no more than ${maxLength} characters`;
               }
               return true;
            }
         }
      });
      };
   }
   validateField(input) {
      const result = this.performValidation(input);
      this.updateValidationUI(input, result);
      return result.isValid;
   }
   performValidation(input) {
      const field = input.closest('.field');
      const value = this.getFieldCheckedValue(input);
   /**
    * Find form element that matches the cached data
    */
   findFormElement(formData) {
      // Try by form_id first (hidden field)
      if (formData.data?.form_id) {
         const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
         if (form) return form;
      if (!value && !input.required) {
         return { isValid: true, message: '' };
      }
      // Try by form_type
      if (formData.data?.form_type) {
         const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
         if (form) return form;
      if (input.required) {
         if (input.type === 'checkbox') {
            if (!input.checked) {
               return { isValid: false, message: 'This field is required' };
            }
         } else if (input.type === 'radio') {
            const radioGroup = document.querySelectorAll(`input[name="${input.name}"]`);
            const anyChecked = Array.from(radioGroup).some(r => r.checked);
            if (!anyChecked) {
               return { isValid: false, message: 'Please select an option' };
            }
         } else if (!value) {
            return { isValid: false, message: 'This field is required' };
         }
      }
      // Fallback: try by formId (if it was already registered)
      return document.querySelector(`[data-form-id="${formData.formId}"]`);
      if(input.checkValidity && !input.checkValidity()){
         return {isValid: false, message: input.validationMessage};
      }
      if (value && Object.hasOwn(field.dataset, 'pattern')) {
         const regex = new RegExp(field.dataset.pattern);
         if (!regex.test(value)) {
            return {isValid: false, message: field.dataset.validationMessage || 'Invalid format'};
         }
      }
      if (Object.hasOwn(field.dataset, 'validate') || input.type) {
         const validator = this.validators[field.dataset.validate||input.type];
         if (validator && validator.pattern && !validator.pattern.test(value)) {
            return {isValid: false, message: validator.message};
         }
         if (validator && validator.test) {
            const result = validator.test(value, field);
            if (result !== true) {
               return {isValid: false, message: result};
            }
         }
      }
      return {isValid: true, message: ''};
   }
   updateValidationUI(input, result) {
      if (result.isValid) {
         this.showSuccess(input, result.message);
      } else {
         this.showError(input, result.message);
      }
   }
   /**
    * Show notification for pending changes
    */
   /**
    * Show notification for pending changes
    */
   showPendingNotification(formId, formData) {
      const formElement = document.querySelector(`[data-form-id="${formId}"]`);
      if (!formElement) return;
      const notification = document.createElement('div');
      notification.className = 'pending-changes-notification';
      notification.innerHTML = `
        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
        <button class="restore-changes" data-form-id="${formId}">Restore</button>
        <button class="discard-changes" data-form-id="${formId}">Discard</button>
    `;
      formElement.insertBefore(notification, formElement.firstChild);
      // Add handlers
      notification.querySelector('.restore-changes').addEventListener('click', async () => {
         await this.restorePendingForm(formId, formData);
         notification.remove();
      });
      notification.querySelector('.discard-changes').addEventListener('click', async () => {
         await this.discardPendingForm(formId);
         notification.remove();
      });
   }
   /**
    * Restore pending form data
    */
   async restorePendingForm(formId, formData) {
      const form = document.querySelector(`[data-form-id="${formId}"]`);
   handleClick(e) {
      let form = this.getForm(e.target);
      if (!form) return;
      // Populate form with cached data
      new this.populateForm(form, formData);
      // Update status in store (mark as restored, not draft)
      await this.store.save({
         formId: formId,
         data: formData,
         status: 'restored',
         timestamp: Date.now()
      });
      if (window.jvbA11y) {
         window.jvbA11y.announce('Previous changes restored');
      }
   }
   /**
    * Discard pending form data
    */
   async discardPendingForm(formId) {
      try {
         await this.store.delete(formId);
         if (window.jvbA11y) {
            window.jvbA11y.announce('Previous changes discarded');
      const itemAction = window.targetCheck(e, '[data-action]');
      if (itemAction) {
         let action = itemAction.dataset.action;
         switch (action) {
            case 'clear-form':
               this.store.delete(form.id);
               form.element.reset();
               form.ui.status.status.hidden = true;
               this.a11y.announce('Form cleared, starting fresh');
               break;
            case 'dismiss-restore':
               form.ui.status.status.hidden = true;
               break;
         }
      } catch (error) {
         console.error('Failed to discard pending form:', error);
      }
   }
   handleChange(e) {
      if (e.target.closest('[data-ignore]') || this.isRestoring) return;
      let field = this.getField(e.target);
      // Check if this input lives inside a collection field
      const collectionField = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
      if (collectionField) {
         // Dependencies still need checking
         if (this.dependencies.has(field.dataset.field)) {
            let dependency = this.dependencies.get(field.dataset.field);
            dependency.items.forEach(item => {
               this.checkFieldDependency(item, field.dataset.field);
            });
         }
         const collectionName = collectionField.dataset.field;
         window.debouncer.schedule(
            `collection:${collectionName}`,
            () => this.updateCollectionField(collectionField),
            150
         );
         return;
      }
      //Dependencies
      if (this.dependencies.has(field.dataset.field)) {
         let dependency = this.dependencies.get(field.dataset.field);
         dependency.items.forEach(item => {
            this.checkFieldDependency(item, field.dataset.field);
         });
      }
      let form = this.getForm(e.target);
      this.updateItem(field.dataset.field, this.getFieldValue(e.target), form);
   }
   handleBlur(e) {
      if (e.target.closest('[data-ignore]') || this.isRestoring) return;
      let form = this.getForm(e.target);
      if (!form) return;
      let field = this.getField(e.target);
      let fieldName = field.dataset.field;
      window.debouncer.cancel(`form:${form.id}:validate:${fieldName}`);
      this.validateField(e.target);
      // If inside a collection, update the whole collection instead
      const collectionField = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
      if (collectionField) {
         this.updateCollectionField(collectionField);
         return;
      }
      this.updateItem(fieldName, this.getFieldValue(e.target), form);
   }
   handleInput(e){
      if (e.target.closest('[data-ignore]') || this.isRestoring) return;
      let form = this.getForm(e.target);
      if (!form) return;
      let field = this.getField(e.target);
      if (!field) return;
      const input = e.target;  // Capture reference
      const fieldName = field.dataset.field;
      // Show pending status regardless of cache
      this.showFormStatus(form.id, 'pending');
      // Debounce validation
      window.debouncer.schedule(
         `form:${form.id}:validate:${fieldName}`,
         () => this.validateField(input),
         500
      );
   }
   async handleSubmit(e) {
      let form = this.getForm(e.target);
      if (!form) return;
      if (this.subscribers.size > 0) {
         e.preventDefault();
         if (form.options.cache) {
            this.cancelBackup();
            await this.backup();
            const storedData = await this.store.get(form.id);
            this.notify('form-submit', {
               config: form,
               data: storedData.changes
            });
         } else {
            this.notify('form-submit', {
               config: form,
               data: this.changes.get(form.id)?.changes??{},
            });
         }
      }
      if (form.options.showSummary) {
         const storedData = await this.store.get(form.id);
         this.showSummary({config: form, changes: storedData?.changes});
      }
   }
   /**
    * Setup global handlers for standalone forms
    * Updates the item, schedules caching if
    * @param name
    * @param value
    * @param form
    */
   initListeners() {
      // Only add if not already added
      if (!this.globalHandlersAdded) {
         document.addEventListener('click', this.clickHandler);
         document.addEventListener('change', this.changeHandler);
         document.addEventListener('blur', this.blurHandler, true);
         document.addEventListener('input', this.inputHandler);
         this.globalHandlersAdded = true;
   updateItem(name, value, form) {
      if (!this.changes.has(form.id)) {
         this.changes.set(form.id, {
            id: form.id,
            timestamp: Date.now(),
            src: window.location.pathname,
            changes: {},
         });
      }
      let changes = this.changes.get(form.id);
      changes.changes[name] = value;
      this.changes.set(form.id, changes);
      if (form.options.cache) {
         this.scheduleBackup();
      }
   }
   scheduleBackup() {
      window.debouncer.schedule(
         `form_changes`,
         async () => {
            if (this.changes.size > 0) {
               await this.backup();
            }
         },
         2000
      );
   }
   cancelBackup() {
      window.debouncer.cancel('form_changes');
   }
   async backup() {
      // Merge with existing stored data
      const toSave = new Map();
      for (let [formId, newData] of this.changes.entries()) {
         const stored = await this.store.get(formId);
         if (stored) {
            // Merge changes
            toSave.set(formId, {
               ...stored,
               ...newData,
               changes: {
                  ...stored.changes,
                  ...newData.changes
               },
               timestamp: Date.now()
            });
         } else {
            toSave.set(formId, newData);
         }
      }
      await this.store.saveMany(toSave);
      for (let formId of this.changes.keys()) {
         this.showFormStatus(formId, 'autosaved');
      }
      this.changes.clear();
   }
   saveCache(formId) {
      if (!this.changes.has(formId)) return;
      let changes = this.changes.get(formId);
      if (changes.size === 0) return;
      this.store.save(changes).then(()=>{});
      this.changes.delete(formId);
   }
   /**
    * Register a standalone form (for front-end forms)
    * Register a form for handling
    * @param {HTMLElement} form
    * @param {object} options
    */
   registerForm(formElement, options = {}) {
      if (!formElement) return;
      const formId = formElement.dataset.formId || `form_${Date.now()}`;
      formElement.dataset.formId = formId;
   registerForm(form, options) {
      //Bail if form already registered
      if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
      formElement.addEventListener('submit', this.submitHandler);
      if (!Object.hasOwn(form.dataset, 'formId')) {
         form.dataset.formId = window.generateID('form_');
      }
      const formId = form.dataset.formId;
      const formConfig = {
         element: formElement,
      this.addFormListeners(form);
      const config = {
         element: form,
         id: formId,
         status: '',
         options: {
            autosave: 'autosave' in formElement.dataset,
            autoUpload: true,
            saveDelay: this.autoSaveDefaults.delay,
            endpoint: formElement.dataset.save ?? '',
            formStatus: true,
            cache: true,
            ...options
            autoUpload: options.autoUpload??false,
            imageMeta: options.imageMeta??true,
            delay: options.delay??1500,
            endpoint: options.save??form.dataset.save??'',
            showStatus: options.showStatus??true,
            showSummary: options.showSummary??false,
            cache: options.cache??true,
            ignore: options.ignore??[]
         },
         dependencies: new Map(),
         data: this.collectFormData(formElement, true),
         ui: window.uiFromSelectors(this.selectors.forms, form)
      };
      this.initializeFormFields(formElement, formConfig);
      this.forms.set(formId, formConfig);
      this.initializeFields(form, config);
      this.forms.set(formId, config);
      // Check for pending data - FIXED
      if (this.store && formConfig.options.cache) {
         const cached = this.store.get(formId);
         if (cached && cached.data) {
            this.showPendingNotification(formId, cached.data);
         }
      }
      return formConfig;
      return config;
   }
      clearForm(formId) {
         const config = this.forms.get(formId);
         if (!config) return;
   /**
    * Initialize all special fields in a form
    */
   initializeFormFields(form, formConfig = null) {
      // Initialize Quill editors
      this.initQuillEditors(form);
      // Initialize repeater fields
      this.initRepeaterFields(form, formConfig);
      this.initTagListFields(form, formConfig);
      // Initialize conditional fields
      if (formConfig) {
         this.initConditionalFields(form, formConfig);
      }
      // Initialize character limits
      this.initCharacterLimits(form);
      // Initialize image upload fields
      this.initImageUploadFields(form, formConfig);
      // Initialize tabs if present
      if (window.jvbTabs && form.querySelector('nav.tabs')) {
         formConfig.tabs = new window.jvbTabs(form);
         this.forms.set(formConfig.formId, formConfig);
         this.initSteppedForm(formConfig.formId);
      }
      // Scan for existing selector fields
      if (window.jvbSelector) {
         window.jvbSelector.scanExistingFields(form);
      }
   }
   /**
    * Initialize stepped form functionality
    */
   initSteppedForm(formId) {
      const formConfig = this.forms.get(formId);
      const form = formConfig.element;
      const tabsInstance = formConfig.tabs;
      const sections = form.querySelectorAll('.tab-content');
      const totalSteps = sections.length;
      const progressBar = form.querySelector('.form-progress .fill');
      const stepText = form.querySelector('.step-text .current');
      const tabButtons = form.querySelectorAll('nav.tabs button');
      // Update progress display
      const updateProgress = (currentStep) => {
         const progress = (currentStep / totalSteps) * 100;
         if (progressBar) {
            progressBar.style.width = progress + '%';
         if (config.unsubscribeTabs) {
            config.unsubscribeTabs();
         }
         if (stepText) {
            stepText.textContent = currentStep;
         if(config.tabs) {
            window.jvbTabs.removeTab(config.element);
         }
         // Update tab states
         tabButtons.forEach((btn, idx) => {
            const stepNum = idx + 1;
            btn.classList.remove('current', 'completed', 'pending');
         if (config.cache && this.changes.has(formId)) this.saveCache(formId);
            if (stepNum < currentStep) {
               btn.classList.add('completed');
            } else if (stepNum === currentStep) {
               btn.classList.add('current');
            } else {
               btn.classList.add('pending');
         // Cleanup items
         for (let [id, input] of this.inputs.entries()) {
            if (input.form === formId) {
               this.inputs.delete(id);
            }
         }
         // Clean up dependencies for this form
         this.dependencies.forEach((dependency, fieldName) => {
            dependency.items = dependency.items.filter(item => item.form !== formId);
            // Remove the dependency entry entirely if no items left
            if (dependency.items.length === 0) {
               this.dependencies.delete(fieldName);
            }
         });
      };
      // Next/Previous button handling
      form.addEventListener('click', (e) => {
         const nextBtn = e.target.closest('[data-action="next-step"]');
         const prevBtn = e.target.closest('[data-action="prev-step"]');
         if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
            const instances = this.quillInstances.get(formId);
            instances.forEach(quillInstance => {
               // Disable the editor
               quillInstance.disable();
         if (nextBtn) {
            e.preventDefault();
            const currentSection = nextBtn.closest('.tab-content');
            const currentStep = parseInt(currentSection.dataset.step);
            const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
               // Remove all event listeners
               quillInstance.off('text-change');
               quillInstance.off('selection-change');
            if (nextSection && this.validateStep(currentSection)) {
               const nextTab = nextSection.dataset.tab;
               tabsInstance.switchTab(nextTab, true);
               updateProgress(currentStep + 1);
               // Get the container elements
               const container = quillInstance.container.parentElement;
               const toolbar = container?.querySelector('.ql-toolbar');
               // Scroll to top of form
               form.scrollIntoView({ behavior: 'smooth', block: 'start' });
            }
         }
               // Remove toolbar
               if (toolbar) {
                  toolbar.remove();
               }
         if (prevBtn) {
            e.preventDefault();
            const currentSection = prevBtn.closest('.tab-content');
            const currentStep = parseInt(currentSection.dataset.step);
            const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
               // Clear the editor content
               quillInstance.setText('');
            if (prevSection) {
               const prevTab = prevSection.dataset.tab;
               tabsInstance.switchTab(prevTab, true);
               updateProgress(currentStep - 1);
               // Scroll to top of form
               form.scrollIntoView({ behavior: 'smooth', block: 'start' });
            }
         }
      });
      // Update progress when tabs are clicked directly
      const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
      tabsInstance.switchTab = (tab, updateHistory) => {
         originalSwitchTab(tab, updateHistory);
         const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
         if (activeSection) {
            const step = parseInt(activeSection.dataset.step);
            updateProgress(step);
         }
      };
      // Initialize progress
      updateProgress(1);
   }
   /**
    * Validate current step before allowing progression
    * Can be enhanced with custom validation rules
    */
   validateStep(section) {
      const fields = section.querySelectorAll('.field');
      let allValid = true;
      fields.forEach(fieldWrapper => {
         const input = fieldWrapper.querySelector('input, textarea, select');
         if (input && !input.closest('[hidden]')) {
            const isValid = this.validateField(input, fieldWrapper);
            if (!isValid) {
               allValid = false;
            }
         }
      });
      return allValid;
   }
   /**
    * Initialize Quill editors
    */
   initQuillEditors(form) {
      window.jvbQuill(form);
   }
   /**
    * Initialize repeater fields
    */
   initRepeaterFields(form, formConfig) {
      form.querySelectorAll('.repeater').forEach(repeater => {
         const addButton = repeater.querySelector('.add-repeater-row');
         const container = repeater.querySelector('.repeater-items');
         const template = repeater.querySelector('template');
         if (!addButton || !template || !container) return;
         // Initialize Sortable for drag-and-drop
         if (window.Sortable) {
            new Sortable(container, {
               handle: '.repeater-row-header',
               animation: 150,
               onEnd: () => {
                  this.updateRepeaterOrder(repeater, formConfig);
               // Remove container
               if (container && container.classList.contains('editor-container')) {
                  const textarea = container.nextElementSibling;
                  if (textarea?.tagName === 'TEXTAREA') {
                     textarea.style.display = '';
                  }
                  container.remove();
               }
            });
            this.quillInstances.delete(formId);
         }
         let checks = {
            repeater: this.repeaters,
            tagList: this.tagLists,
            charLimit: this.charLimits,
            quantity: this.quantityFields
         };
         for (let [type, check] of Object.entries(checks)) {
            if (check.size === 0) continue;
            let hasAny = Array.from(check.values()).filter(item => item.form === formId);
            if (hasAny.length > 0) {
               hasAny.forEach(item => {
                  switch (type) {
                     case 'repeater':
                        this.removeRepeaterListeners(item.element);
                        break;
                     case 'tagList':
                        this.removeTagListListeners(item.element);
                        break;
                     case 'charLimit':
                        this.removeCharacterLimitListeners(item.element);
                        break;
                     case 'quantity':
                        this.removeQuantityListeners(item.element);
                        break;
                  }
         // Add row handler
         addButton.addEventListener('click', () => {
            this.addRepeaterRow(repeater, formConfig);
         });
         // Remove row handlers
         container.addEventListener('click', (e) => {
            if (e.target.closest('.remove-row')) {
               this.removeRepeaterRow(e.target.closest('.repeater-row'), formConfig);
                  if (check.has(item.id)) {
                     check.delete(item.id);
                  }
               });
            }
         });
      });
   }
   /**
    * Add repeater row
    */
   addRepeaterRow(repeater, formConfig) {
      const container = repeater.querySelector('.repeater-items');
      const template = repeater.querySelector('template');
      const index = container.children.length;
      const fieldName = repeater.dataset.field;
      // Clone template
      const row = template.content.cloneNode(true).firstElementChild;
      row.dataset.index = index;
      // Update field names
      row.querySelectorAll('input, select, textarea').forEach(field => {
         const originalName = field.name;
         field.name = `${fieldName}:${index}:${originalName}`;
         field.id = `${fieldName}-${index}-${originalName}`;
         // Update label if exists
         const label = field.nextElementSibling;
         if (label && label.tagName === 'LABEL') {
            label.htmlFor = field.id;
         }
      });
      container.appendChild(row);
      if (formConfig) {
         this.scheduleSave(formConfig, {
            type: 'repeater',
            action: 'add',
            fieldName: fieldName,
            delay: this.repeaterDelays.add
         });
         this.removeFormListeners(config.element);
         this.forms.delete(formId);
         window.debouncer.cancel(`form_changes`);
      }
      defineSummaryTemplate() {
         this.summaryTemplate = true;
         let form = this;
         this.templates.define(
            'formSummary',
            {
               refs: {
                  result: '.result',
                  h3: 'h3',
                  p: 'p',
               },
               setup({ el, refs, manyRefs, data }) {
                  const skipFields = ['sendAll', ...data.config.options.ignore??[]];
      if (window.jvbA11y) {
         window.jvbA11y.announce('Row added');
      }
   }
                  for (let [key, value] of Object.entries(data.changes)) {
                     if (skipFields.includes(key) || form.isEmptyValue(value)) continue;
   /**
    * Remove repeater row
    */
   removeRepeaterRow(row, formConfig) {
      const repeater = row.closest('.repeater');
      const fieldName = repeater.dataset.field;
                     let input = Array.from(form.inputs.values())
                        .find(temp => temp.field?.dataset.field === key);
                     if (!input) continue;
      row.remove();
                     let entry = refs.result.cloneNode(true);
                     let title = entry.querySelector('h3');
                     let p = entry.querySelector('p');
      // Reindex remaining rows
      this.updateRepeaterOrder(repeater, formConfig);
                     // Get field label - prioritize legend for fieldsets, then label
                     const legend = input.field?.querySelector('legend');
                     title.textContent = legend
                        ? legend.textContent.replace('*', '').trim()
                        : input.ui.label?.textContent.replace('*', '').trim();
      // Schedule save
      if (formConfig) {
         this.scheduleSave(formConfig, {
            type: 'repeater',
            action: 'remove',
            fieldName: fieldName,
            delay: this.repeaterDelays.remove
         });
      }
      if (window.jvbA11y) {
         window.jvbA11y.announce('Row removed');
      }
   }
                     const formattedValue = form.formatValueForSummary(value, input);
   /**
    * Update repeater order after sorting
    */
   updateRepeaterOrder(repeater, formConfig) {
      const container = repeater.querySelector('.repeater-items');
      const fieldName = repeater.dataset.field;
                     if (formattedValue instanceof HTMLElement) {
                        // If it's an HTML element (repeater, tag-list, etc.), replace <p>
                        p.replaceWith(formattedValue);
                     } else {
                        // If it's a string, set text content
                        p.textContent = formattedValue;
                     }
      // Reindex all rows
      Array.from(container.children).forEach((row, index) => {
         row.dataset.index = index;
                     el.append(entry);
                  }
                  let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
                  if (uploads) {
                     uploads.forEach(upload => {
                        let label = upload.querySelector('h2')?.textContent??'Upload:';
                        let imgs = upload.querySelectorAll('.item-grid.preview img');
                        let field = refs.result.cloneNode(true);
                        if (imgs) {
                           let entry = refs.result.cloneNode(true);
                           let title = field.querySelector('h3');
                           let p = field.querySelector('p');
                           p?.remove();
                           if (title) title.textContent = label;
                           imgs.forEach(img => {
                              img = img.cloneNode(true);
                              entry.append(img);
                           });
                           el.append(entry);
                        }
                     });
                  }
         // Update field names
         row.querySelectorAll('input, select, textarea').forEach(field => {
            const parts = field.name.split(':');
            if (parts.length === 3) {
               const originalName = parts[2];
               field.name = `${fieldName}:${index}:${originalName}`;
               field.id = `${fieldName}-${index}-${originalName}`;
               // Update label
               const label = field.nextElementSibling;
               if (label && label.tagName === 'LABEL') {
                  label.htmlFor = field.id;
                  refs.result?.remove();
                  data.config.element.after(el);
                  window.fade(data.config.element, false);
               }
            }
         });
      });
      // Schedule save
      if (formConfig) {
         this.scheduleSave(formConfig, {
            type: 'repeater',
            action: 'reorder',
            fieldName: fieldName,
            delay: this.repeaterDelays.reorder
         });
         );
      }
   }
   /**
    * Initialize tag list fields
    */
   initTagListFields(form, formConfig) {
      form.querySelectorAll('.field.tag-list').forEach(field => {
         const inputRow = field.querySelector('.tag-input-row');
         const addButton = field.querySelector('.add-tag-item');
         const tagsContainer = field.querySelector('.tag-items');
         const template = field.querySelector('.tag-template');
         const fieldName = field.dataset.field;
         const tagFormat = field.dataset.tagFormat || 'first_field';
         if (!inputRow || !addButton || !tagsContainer || !template) return;
         // Get all input fields in the input row (excluding the button)
         const getInputFields = () => {
            return Array.from(inputRow.querySelectorAll('input, select, textarea'))
               .filter(input => !input.closest('button'));
      initializeFields(container, config = null) {
         const fieldHandlers = {
            '[data-editor]': () => this.checkForQuill(container,config),
            'div.quantity': () => this.checkForQuantity(container),
            '.repeater': () => this.checkForRepeaters(container, config),
            '.field.tag-list': () => this.checkForTagLists(container),
            '[data-depends-on]': () => this.checkForConditionalFields(container),
            '[data-limit]': () => this.checkForCharacterLimits(container),
            '[data-uploader],[data-upload-field]': () => this.checkForImageUploads(container, config),
            'nav.tabs': () => this.checkForTabs(container, config),
            '[data-type="selector"]': () => this.checkForSelectors(container)
         };
         // Add tag handler
         const addTag = () => {
            const inputs = getInputFields();
            const data = {};
            let hasValue = false;
         for (const [selector, handler] of Object.entries(fieldHandlers)) {
            if (container.querySelector(selector)) {
               handler();
            }
         }
            // Collect values from inputs
            inputs.forEach(input => {
               const fieldName = input.name.replace('new_', '');
         let inputs = Array.from(container.querySelectorAll(this.inputSelectors))
            .filter(input => !input.closest('.ql-clipboard'));
         inputs.map(input => {
            this.getItem(input, config?.id);
         });
      }
         checkForQuill(form, config) {
            if (!form.querySelector('[data-editor]')) return;
            if (config && !Object.hasOwn(config, 'hasQuill')){
               config.hasQuill = true;
               this.forms.set(config.id, config);
            }
            if (!this.quillInstances.has(config.id)) {
               this.quillInstances.set(config.id, new Set());
            }
            const instances = window.jvbQuill(form);
            instances.forEach(instance => {
               this.quillInstances.get(config.id).add(instance);
            });
         }
         checkForQuantity(form) {
            if (!form.querySelector(this.selectors.number.number)) return;
            form.querySelectorAll(this.selectors.number.number).forEach(num => {
               let config = {
                  id: window.generateID('quant'),
                  form: form.dataset.formId,
                  ui: window.uiFromSelectors(this.selectors.number, num),
                  element: num
               };
               num.dataset.numId = config.id;
               this.quantityFields.set(config.id, config);
               this.addQuantityListeners(num);
            });
         }
            addQuantityListeners(el) {
               el.addEventListener('click', this.quantityClick);
            }
            removeQuantityListeners(el) {
               el.removeEventListener('click', this.quantityClick);
            }
            handleQuantityClick(e) {
               let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
               if(!conf) return;
               let change = 0;
               if (conf.ui.increase.contains(e.target)) {
                  change++;
               } else if (conf.ui.decrease.contains(e.target)) {
                  change--;
               }
               if (change === 0) return;
               let field = this.getField(e.target);
               let step = conf.ui.input.step;
               step = Math.max(step, 1);
               if (e.ctrlKey && e.shiftKey) {
                  step = step * 50;
               } else if (e.ctrlKey) {
                  step = step *5;
               } else if (e.shiftKey) {
                  step = step * 10;
               }
               let value = (conf.ui.input.value === '') ? 0 : parseFloat(conf.ui.input.value);
               conf.ui.input.value = (value + (step * change));
               value = parseFloat(conf.ui.input.value);
               if (conf.ui.input.min && value < conf.ui.input.min) {
                  conf.ui.input.value = conf.ui.input.min;
                  conf.ui.decrease.disabled = true;
               } else if (conf.ui.input.max && value > conf.ui.input.max) {
                  conf.ui.input.value = conf.ui.input.max;
                  conf.ui.increase.disabled = true;
               } else {
                  if (conf.ui.decrease.disabled) conf.ui.decrease.disabled = false;
                  if (conf.ui.increase.disabled) conf.ui.increase.disabled = false;
               }
            }
         checkForRepeaters(form) {
            if (!form.querySelector(this.selectors.repeater.repeater)) return;
            form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
               let config = {
                  id: repeater.querySelector('template').className??window.generateID('repeater'),
                  ui: window.uiFromSelectors(this.selectors.repeater, repeater),
                  form: form.dataset.formId,
                  element: repeater,
                  field: this.getField(repeater),
                  sortable: false,
                  rows: []
               };
               if (!config.ui.add) return;
               let template = repeater.querySelector('template');
               this.templates.define(
                  template.className,
                  {
                     manyRefs: {
                        inputs: this.inputSelectors,
                     },
                     setup({el, refs, manyRefs, data}) {
                        let index = config.ui.items?.children?.length??0;
                        el.dataset.index = index;
                        manyRefs.inputs?.forEach(input => {
                           window.prefixInput(input, `${data.repeater.dataset.field}:${index}:`, el, false, true);
                        });
                     }
                  },
               );
               if (window.Sortable) {
                  config.sortable = new Sortable(repeater, {
                     handle: this.selectors.repeater.header,
                     animation: 150,
                     onEnd: () => {
                        this.reindexList(repeater);
                     }
                  });
               }
               repeater.dataset.repeaterId = config.id;
               this.addRepeaterListeners(repeater);
               this.repeaters.set(config.id, config);
            });
         }
            addRepeaterListeners(el) {
               el.addEventListener('click', this.repeaterClick);
            }
            removeRepeaterListeners(el) {
               el.removeEventListener('click', this.repeaterClick);
            }
            handleRepeaterClick(e) {
               if (e.target.matches(this.selectors.repeater.add)) {
                  this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
               } else if (e.target.matches(this.selectors.repeater.remove)) {
                  this.removeRepeaterRow(e.target.closest('[data-index]'));
               }
            }
            addRepeaterRow(repeater) {
               let data = {};
               data.repeater = repeater;
               let config = this.repeaters.get(repeater.dataset.repeaterId);
               let row = this.templates.create(repeater.dataset.repeaterId, data);
               config.rows.push({
                  element: row,
                  fields: Array.from(row.querySelectorAll('[data-field]'))
               });
               this.repeaters.set(config.id, config);
               config.ui.items.append(row);
               let form = this.getForm(repeater);
               this.initializeFields(repeater, form);
               this.a11y.announce('Row added');
            }
            removeRepeaterRow(row) {
               let repeater = row.closest('[data-repeater-id]');
               row.remove();
               this.reindexList(repeater);
               this.a11y.announce('Row removed');
            }
         checkForTagLists(form) {
            form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
               let config = {
                  id: field.querySelector('template').className??window.generateID('tagList'),
                  ui: window.uiFromSelectors(this.selectors.tagList, field),
                  element: field,
                  form: form.dataset.formId,
                  format: field.dataset.tagFormat??'first_field'
               };
               if (!config.ui.input || !config.ui.add || !config.ui.items) return;
               field.dataset.tagListId = config.id;
               config.fieldName = field.dataset.field;
               let template = field.querySelector('template');
               this.templates.define(
                  template.className,
                  {
                     refs: {
                        label: this.selectors.tagList.label,
                     },
                     manyRefs: {
                        inputs: this.inputSelectors,
                     },
                     setup({el, refs, manyRefs, data}) {
                        let index = config.ui.items?.children?.length??0;
                        el.dataset.index = index;
                        manyRefs.inputs?.forEach(input => {
                           let wrapper = input.closest('.tag-item');
                           window.prefixInput(input, `${data.fieldName}:${index}:`, wrapper, false, true)
                        });
                        if (refs.label) {
                           refs.label.textContent = data.label;
                        }
                     }
                  },
               );
               config.ui.inputs = Array.from(field.querySelectorAll(this.selectors.tagList.inputs));
               config.ui.value = Array.from(field.querySelectorAll(this.selectors.tagList.value));
               this.tagLists.set(config.id, config);
               this.addTagListListeners(field);
            });
         }
            addTagListListeners(el) {
               el.addEventListener('click', this.tagListClick);
               el.addEventListener('keypress', this.tagListInput);
            }
            removeTagListListeners(el) {
               el.removeEventListener('click', this.tagListClick);
               el.removeEventListener('keypress', this.tagListInput);
            }
            handleTagListClick(e) {
               if (window.targetCheck(e,this.selectors.tagList.add)) {
                  this.addTagListItem(e.target.closest('[data-tag-list-id]'));
               } else if (window.targetCheck(e, this.selectors.tagList.remove)) {
                  this.removeTagListItem(e.target.closest(this.selectors.tagList.item));
               }
            }
         addTagListItem(tagList) {
            let config = this.tagLists.get(tagList.dataset.tagListId);
            if (!config) return;
            let data = {};
            let hasValue = false;
            let isValid = true;
            // First pass: validate all inputs
            for (let input of config.ui.inputs) {
               const isRequired = input.required || input.dataset.required === 'true';
               const value = this.getFieldValue(input);
               if (value) hasValue = true;
               // Validate and check for errors
               const valid = this.validateField(input);
               if (isRequired && !value) {
                  this.showError(input, 'This field is required');
                  isValid = false;
               } else if (!valid) {
                  isValid = false;
               }
               const fieldName = input.name.replace('new_','');
               data[fieldName] = value;
            });
            }
            // Stop if validation failed
            if (!isValid) {
               this.a11y.announce('Please correct the errors before adding');
               const firstInvalid = config.ui.inputs.find(input => {
                  const isRequired = input.required || input.dataset.required === 'true';
                  return (isRequired && !this.getFieldValue(input));
               });
               if (firstInvalid) firstInvalid.focus();
               return;
            }
            if (!hasValue) {
               if (window.jvbA11y) {
                  window.jvbA11y.announce('Please fill in at least one field', 'error');
               }
               inputs[0].focus();
               this.a11y.announce('Please fill in at least one field');
               config.ui.inputs[0].focus();
               return;
            }
            // Validate required fields using data-required attribute
            const invalidField = inputs.find(input => {
               const isRequired = ('required' in input.dataset && input.dataset.required === '1');
               const value = this.getFieldValue(input);
               return isRequired && !value;
            // Build label
            let label;
            switch (config.format) {
               case 'first_field':
                  label = Object.values(data)[0];
                  break;
               case 'all_fields':
                  label = Object.values(data).join(', ');
                  break;
               default:
                  if (config.format.includes('{')) {
                     label = config.format;
                     for (const [key, value] of Object.entries(data)) {
                        label = label.replace(`{${key}}`, value);
                     }
                  } else {
                     label = data[config.format]??Object.values(data)[0];
                  }
                  break;
            }
            let newItem = this.templates.create(tagList.dataset.tagListId, {
               label: label,
               fieldName: config.fieldName
            });
            if (invalidField) {
               const fieldWrapper = invalidField.closest('.field');
               const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
               this.showError(fieldWrapper, `${fieldLabel} is required.`);
               invalidField.focus();
               return;
            }
            for (let input of inputs) {
               let wrapper = field.closest('.field');
               if (!this.validateField(input, wrapper)){
                  input.focus();
                  return;
               }
            }
            // Clone template and populate
            const index = tagsContainer.children.length;
            const newTag = template.content.cloneNode(true).firstElementChild;
            newTag.dataset.index = index;
            // Update tag label
            const tagLabel = newTag.querySelector('.tag-label');
            if (tagLabel) {
               tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
            }
            // Update hidden inputs
            newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
            const index = config.ui.items?.children?.length ?? 0;
            newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
               const fieldKey = input.dataset.field;
               input.name = `${fieldName}:${index}:${fieldKey}`;
               input.name = `${config.fieldName}:${index}:${fieldKey}`;
               input.id = `${config.fieldName}:${index}:${fieldKey}`;
               input.value = data[fieldKey] || '';
            });
            tagsContainer.appendChild(newTag);
            config.ui.items.append(newItem);
            // Clear inputs
            inputs.forEach(input => {
               if (input.type === 'checkbox' || input.type === 'radio') {
            // Clear inputs AFTER success
            for (let input of config.ui.inputs) {
               if (['checkbox', 'radio'].includes(input.type)) {
                  input.checked = false;
               } else {
                  input.value = '';
               }
               let field = input.closest('.field');
               this.clearValidation(field);
            });
            // Focus first input
            if (inputs.length > 0) {
               inputs[0].focus();
               this.clearValidation(input);
            }
            // Schedule save
            if (formConfig) {
               this.scheduleSave(formConfig, {
                  type: 'tag_list',
                  action: 'add',
                  fieldName: fieldName,
                  delay: this.autoSaveDefaults.delay
               });
            config.ui.inputs[0]?.focus();
            this.updateCollectionField(tagList);
            this.a11y.announce('Item added');
         }
            removeTagListItem(item) {
               let tagList = item.closest('[data-tag-list-id]');
               if (!tagList) return;
               item.remove();
               this.reindexList(tagList);
               this.updateCollectionField(tagList);
               this.a11y.announce('Item removed');
            }
            handleTagListInput(e) {
               let target = e.target;
               let field = target.closest('[data-tag-list-id]');
               if (!field) return;
               let config = this.tagLists.get(field.dataset.tagListId);
               if (!config) return;
            if (window.jvbA11y) {
               window.jvbA11y.announce('Item added');
            }
         };
         // Add button click
         addButton.addEventListener('click', addTag);
         // Enter key support on last input
         const inputs = getInputFields();
         if (inputs.length > 0) {
            // Tab through inputs, Enter on last one adds the tag
            inputs[inputs.length - 1].addEventListener('keypress', (e) => {
               if (e.key === 'Enter') {
                  e.preventDefault();
                  addTag();
               }
            });
            // Enter on other inputs moves to next field
            inputs.slice(0, -1).forEach((input, i) => {
               input.addEventListener('keypress', (e) => {
                  if (e.key === 'Enter') {
                  if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
                     e.preventDefault();
                     inputs[i + 1].focus();
                     this.addTagListItem(target.closest('[data-tag-list-id]'));
                  } else {
                     e.preventDefault();
                     let index = config.ui.inputs.indexOf(target);
                     config.ui.inputs[index+1].focus();
                  }
               }
            }
         checkForConditionalFields(form) {
            form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
               const dependsOn = field.dataset.dependsOn;
               const requiredValue = field.dataset.dependsValue;
               const operator = field.dataset.dependsOperatior??'==';
               if (!this.dependencies.has(dependsOn)) {
                  let element = document.querySelector(`[field="${dependsOn}"]`);
                  if (element) {
                     this.dependencies.set(dependsOn, {
                        element: element,
                        items: []
                     });
                  }
               }
               let dependency = this.dependencies.get(dependsOn);
               dependency.items.push({
                  field: field,
                  form: form.dataset.formId,
                  requiredValue: requiredValue,
                  operator: operator
               });
               this.dependencies.set(dependsOn, dependency);
               this.checkFieldDependency(dependency, dependsOn);
            });
         }
            checkFieldDependency(dependentField, controlFieldName) {
               const controlField = this.dependencies.get(controlFieldName);
               if (!controlField) return;
               const controlValue = this.getFieldCheckedValue(controlField.element);
               const shouldShow = this.evaluateCondition(
                  controlValue,
                  dependentField.requiredValue,
                  dependentField.operator
               );
               this.toggleFieldVisibility(dependentField.field, shouldShow);
            }
            evaluateCondition(value, requiredValue, operator) {
               const fieldStr = String(value || '');
               const requiredStr = String(requiredValue || '');
               switch (operator) {
                  case '==': return fieldStr === requiredStr;
                  case '!=': return fieldStr !== requiredStr;
                  case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
                  case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
                  case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
                  case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
                  case 'contains': return fieldStr.includes(requiredStr);
                  case 'empty': return fieldStr === '';
                  case 'not_empty': return fieldStr !== '';
                  default: return fieldStr === requiredStr;
               }
            }
            toggleFieldVisibility(field, show) {
               const wrapper = field.closest('.field, fieldset');
               if (!wrapper) return;
               wrapper.hidden = !show;
               wrapper.querySelectorAll('input, select, textarea').forEach(control => {
                  control.disabled = !show;
                  if (!show && control.hasAttribute('required')) {
                     control.dataset.wasRequired = 'true';
                     control.removeAttribute('required');
                  } else if (show && control.dataset.wasRequired === 'true') {
                     control.setAttribute('required', '');
                     delete control.dataset.wasRequired;
                  }
               });
            });
         }
         // Remove tag handler
         tagsContainer.addEventListener('click', (e) => {
            if (e.target.closest('.remove-tag')) {
               const tag = e.target.closest('.tag-item');
               const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
               tag.remove();
               // Reindex remaining tags
               this.reindexTagList(tagsContainer, fieldName);
               // Schedule save
               if (formConfig) {
                  this.scheduleSave(formConfig, {
                     type: 'tag_list',
                     action: 'remove',
                     fieldName: fieldName,
                     delay: this.autoSaveDefaults.delay
                  });
               }
               if (window.jvbA11y) {
                  window.jvbA11y.announce(`${tagText} removed`);
               }
            }
         });
      });
   }
   checkForCharacterLimits(form) {
      if (!form.querySelector(this.selectors.limits.hasLimit)) return;
      this.countUpdaters = this.updateCount.bind(this);
   /**
    * Reindex tag list items
    */
   reindexTagList(container, baseFieldName) {
      Array.from(container.children).forEach((tag, index) => {
         tag.dataset.index = index;
      form.querySelectorAll(this.selectors.limits.hasLimit).forEach(field => {
         const input = this.getFieldInput(field);
         if (!input) return;
         tag.querySelectorAll('input[type="hidden"]').forEach(input => {
            const fieldKey = input.dataset.field;
            input.name = `${baseFieldName}:${index}:${fieldKey}`;
         });
      });
   }
         let id = window.generateID('limit');
         input.dataset.charLimitId = id;
         input.dataset.limit = field.dataset.maxlength;
   /**
    * Get display text for tag based on format
    */
   getTagDisplayText(data, format) {
      const values = Object.values(data).filter(v => v);
      if (values.length === 0) return 'New Item';
      switch (format) {
         case 'first_field':
            return values[0];
         case 'all_fields':
            return values.join(', ');
         default:
            // Template format like "{name} ({email})"
            if (format.includes('{')) {
               let text = format;
               for (const [key, value] of Object.entries(data)) {
                  text = text.replace(`{${key}}`, value);
               }
               return text;
            }
            // Use specific field
            return data[format] || values[0];
      }
   }
   /**
    * HTML escape helper
    */
   escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
   }
   /**
    * Initialize conditional fields
    */
   initConditionalFields(form, formConfig) {
      form.querySelectorAll('[data-depends-on]').forEach(field => {
         const dependsOn = field.dataset.dependsOn;
         const requiredValue = field.dataset.dependsValue;
         const operator = field.dataset.dependsOperator || '==';
         // Store dependency
         if (!formConfig.dependencies.has(dependsOn)) {
            formConfig.dependencies.set(dependsOn, []);
         }
         formConfig.dependencies.get(dependsOn).push({
            field: field,
            requiredValue: requiredValue,
            operator: operator
         });
         // Check initial state
         this.checkFieldDependency(form, field, dependsOn, requiredValue, operator);
      });
   }
   /**
    * Check field dependency
    */
   checkFieldDependency(form, field, dependsOn, requiredValue, operator) {
      const triggerField = form.querySelector(`[name="${dependsOn}"]`);
      if (!triggerField) return;
      const value = this.getFieldValue(triggerField);
      const shouldShow = this.evaluateCondition(value, requiredValue, operator);
      this.toggleFieldVisibility(field, shouldShow);
   }
   /**
    * Evaluate conditional operator
    */
   evaluateCondition(value, requiredValue, operator) {
      const fieldStr = String(value || '');
      const requiredStr = String(requiredValue || '');
      switch (operator) {
         case '==': return fieldStr === requiredStr;
         case '!=': return fieldStr !== requiredStr;
         case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
         case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
         case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
         case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
         case 'contains': return fieldStr.includes(requiredStr);
         case 'empty': return fieldStr === '';
         case 'not_empty': return fieldStr !== '';
         default: return fieldStr === requiredStr;
      }
   }
   /**
    * Toggle field visibility
    */
   toggleFieldVisibility(field, show) {
      const wrapper = field.closest('.field, fieldset');
      if (!wrapper) return;
      wrapper.hidden = !show;
      wrapper.querySelectorAll('input, select, textarea').forEach(control => {
         control.disabled = !show;
         if (!show && control.hasAttribute('required')) {
            control.dataset.wasRequired = 'true';
            control.removeAttribute('required');
         } else if (show && control.dataset.wasRequired === 'true') {
            control.setAttribute('required', '');
            delete control.dataset.wasRequired;
         }
      });
   }
   /**
    * Initialize character limits
    */
   initCharacterLimits(form) {
      form.querySelectorAll('[data-limit]').forEach(input => {
         const limit = parseInt(input.dataset.limit, 10);
         const field = input.closest('.field');
         // Create counter if it doesn't exist
         let counter = field?.querySelector('.char-count');
         if (!counter && field) {
            counter = document.createElement('div');
            counter.className = 'char-count';
            counter.innerHTML = `<span class="current">0</span> / <span class="limit">${limit}</span>`;
            field.appendChild(counter);
         }
         const updateCount = () => {
            const length = input.value.length;
            if (counter) {
               counter.querySelector('.current').textContent = length;
               counter.classList.toggle('exceeded', length > limit);
            }
            // Truncate if exceeds limit
            if (length > limit) {
               input.value = input.value.substring(0, limit);
               if (counter) {
                  counter.querySelector('.current').textContent = limit;
               }
            }
         let config = {
            element: input,
            form: form.dataset.formId,
            ui: window.uiFromSelectors(this.selectors.limits, field)
         };
         input.addEventListener('input', updateCount);
         updateCount(); // Initial count
         if (config.ui.limit) {
            config.ui.limit.textContent = field.dataset.maxlength;
         }
         this.charLimits.set(id, config);
         this.addCharacterLimitListeners(input);
      });
   }
            addCharacterLimitListeners(input) {
               input.addEventListener('input', this.countUpdaters, {passive: true});
            }
            removeCharacterLimitListeners(input) {
               input.removeEventListener('input', this.countUpdaters, {passive: true});
            }
            updateCount(e) {
               let target = e.target;
               let config = this.charLimits.get(target.dataset.charLimitId);
               if (!config) return;
               let length = target.value.length;
               let limit = target.dataset.limit;
               if (config.ui.current) {
                  config.ui.current.textContent = length;
                  config.ui.current.classList.toggle('exceeded', length >= limit);
               }
               if (length > limit) {
                  target.value = target.value.slice(0, limit);
               }
            }
         checkForImageUploads(form, config) {
            window.jvbUploads.scanFields(form, config.options.autoUpload, config.options.imageMeta);
         }
         checkForTabs(form, config) {
            if (window.jvbTabs && form.querySelector('nav.tabs')) {
               config.tabs = window.jvbTabs.registerTab(form, {
                  preCheck: (section, tabConfig) => {
                     return this.validateStep(section, config);
                  }
               });
               config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form);
               config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections));
               config.ui.tabs.inputs = {};
               config.ui.tabs.sections.forEach(section => {
                  config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs));
               });
               config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
               config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
                  if (event === 'tab-switched') {
                     if (config.ui.tabs.progress) {
                        const section = config.ui.tabs.sections.filter(section => section.dataset.tab === data.current)[0]??false;
                        if (!section) return;
                        const step = section.dataset.step;
                        const total = config.ui.sections.length;
                        window.showProgress(
                           config.ui.tabs.progress,
                           step,
                           total
                        );
                     }
                  }
               });
               this.forms.set(config.id, config);
            }
         }
         validateStep(section, config) {
            const formId = section.closest('[data-form-id]')?.dataset.formId;
            if (!formId) return true;
            const form = this.forms.get(formId);
            if (!form) return true;
            const inputs = Array.from(this.inputs.values())
               .filter(item =>
                  item &&
                  item.form === formId &&
                  item.section === section.dataset.tab &&
                  !item.element.closest('[hidden]')
               );
            return inputs.every(item => this.validateField(item.element) === true);
         }
         checkForSelectors(form) {
            if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
         }
   /**
    * Mainly for repeaters or taglist
    * @param {HTMLElement} container
    */
   reindexList(container) {
      const fieldName = container.dataset.field || container.dataset.repeaterId || container.dataset.tagListId;
      Array.from(container.children).forEach((item, index) => {
         item.dataset.index = `${index}`;
         // Find ALL inputs within this item, not just direct children
         const inputs = item.querySelectorAll('input, select, textarea');
         inputs.forEach(input => {
            // Skip inputs that shouldn't be re-indexed (like file inputs)
            if (input.type === 'file') return;
            // Get the field name from the input's data-field or name
            const inputField = input.dataset.field || input.name.split(':').pop();
            // Re-prefix with the new index, passing item as wrapper
            window.prefixInput(
               input,
               `${fieldName}:${index}:`,
               item,
               false,
               true
            );
         });
      });
      this.updateCollectionField(container);
   }
   /**
    * Initialize image upload fields
    * Update the entire repeater/tagList field data
    * Call this whenever rows are added, removed, or reordered
    */
   initImageUploadFields(form, config) {
      window.jvbUploads.scanFields(form, config.options.autoUpload);
   updateCollectionField(element) {
      const field = element.closest('[data-field]');
      if (!field) return;
      const fieldType = field.dataset.fieldType;
      if (!['repeater', 'tag-list'].includes(fieldType)) return;
      const form = this.getForm(element);
      if (!form) return;
      // Get all current data for the collection
      const value = this.getFieldValue(field);
      this.updateItem(field.dataset.field, value, form);
   }
   /**********************************************************************
    VALIDATION
   **********************************************************************/
   //text, email, url, tel, date, time, datetime, number
   //select, checkbox, radio, true_false
   //textarea
   //repeater: subfields validation; no submission until all required are entered
   //tag fields: similar to repeater; each separate field is its own hidden field
   //upload: comma separated ints
   //selector: comma separated ints
   //location: hidden inputs for address, lat, lng, street, city, province, postal_code, country
   clearValidation(input) {
      let field = this.getField(input);
      if (!field) return;
      let item = this.getItem(input);
      if (!item) return;
      field.classList.remove('has-error', 'has-success');
      if (item.ui.success) item.ui.success.hidden = true;
      if (item.ui.error) item.ui.error.hidden = true;
      if (item.ui.message) {
         item.ui.message.hidden = true;
         item.ui.message.textContent = '';
      }
   }
   /* ========== Event Handlers ========== */
   showError(input, message = 'Invalid field') {
      let field = this.getField(input);
      if (!field) return;
      let item = this.getItem(input);
      if (!item) return;
   async handleSubmit(event) {
      const form = event.target;
      field.classList.remove('has-success');
      field.classList.add('has-error');
      if (!form.dataset.formId) return;
      const formConfig = this.forms.get(form.dataset.formId);
      if (item.ui.success) item.ui.success.hidden = true;
      if (item.ui.error) item.ui.error.hidden = true;
      if (item.ui.message) {
         item.ui.message.hidden = false;
         item.ui.message.textContent = message;
      }
   }
      // Handle subscriber-based forms
      if (this.subscribers.size > 0) {
         event.preventDefault();
         const formData = this.collectFormData(form);
   showSuccess(input, message = '') {
      let field = this.getField(input);
      if (!field) return;
      let item = this.getItem(input);
      if (!item) return;
         // Notify subscribers (they'll handle actual submission)
         this.notify('form-submit', {
            formId: form.dataset.formId,
            fullData: formData,
            config: formConfig
         });
      field.classList.remove('has-error');
      field.classList.add('has-success');
      if (item.ui.success) item.ui.success.hidden = false;
      if (item.ui.error) item.ui.error.hidden = true;
      if (item.ui.message) {
         item.ui.message.hidden = message=== '';
         item.ui.message.textContent = message;
      }
   }
@@ -1050,11 +1452,6 @@
      if (window.jvbA11y) {
         window.jvbA11y.announce(data.message || 'Form submitted successfully');
      }
      // Trigger custom event
      form.dispatchEvent(new CustomEvent('jvb-form-success', {
         detail: data
      }));
   }
   handleFormError(form, data) {
@@ -1073,28 +1470,20 @@
      if (data.field) {
         const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
         if (fieldWrapper) {
            // Use existing showError method for consistency
            this.showError(fieldWrapper, data.message);
            // Mark as touched so validation persists
            this.touchedFields.add(data.field);
            // Scroll to error
            fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
            // Focus the input for better UX
            const input = fieldWrapper.querySelector('input, textarea, select');
            if (input) {
               input.focus();
            }
         }
      } else {
         // General form error (not field-specific)
         const error = document.createElement('div');
         error.className = 'form-error error-message';
         error.textContent = data.message;
         // Add icon for consistency
         const icon = window.getIcon?.('close-circle');
         if (icon) {
            icon.classList.add('error-icon');
@@ -1102,12 +1491,9 @@
         }
         form.insertBefore(error, form.firstChild);
         // Scroll to top to show the error
         form.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
      // Announce error for accessibility
      if (window.jvbA11y) {
         const announcement = data.field
            ? `Error in ${data.field}: ${data.message}`
@@ -1115,1352 +1501,545 @@
         window.jvbA11y.announce(announcement);
      }
      // Trigger custom event
      form.dispatchEvent(new CustomEvent('jvb-form-error', {
         detail: data
      }));
   }
   handleClick(e) {
      if (window.targetCheck(e, 'div.quantity')) {
         let container = window.targetCheck(e, 'div.quantity');
         this.handleNumberClick(e, container.querySelector('input'));
      } else if (window.targetCheck(e, '[data-action]')) {
         let actionEl = window.targetCheck(e, '[data-action]');
         let action = actionEl.dataset.action;
         let form = actionEl.closest('form');
         switch (action) {
            case 'clear-form':
               if (form?.dataset.formId) {
                  this.store.delete(form.dataset.formId);
                  form.reset();
                  // Hide the status message
                  form.querySelector('.fstatus').hidden = true;
               }
               if (window.jvbA11y) {
                  window.jvbA11y.announce('Form cleared, starting fresh');
               }
               break;
            case 'dismiss-restore':
               form.querySelector('.fstatus').hidden = true;
               break;
         }
      }
   }
   handleNumberClick(e, input) {
      let change = 0;
      if (e.target.closest('.increase')) {
         change += 1;
      } else if (e.target.closest('.decrease')) {
         change -=1;
      }
      if (change !== 0) {
         let step = parseFloat(input.step);
         //Allow for cents, but default to increasing by 1
         step = Math.max(step, 1);
         if(e.ctrlKey && e.shiftKey) {
            step = step * 50;
         } else if (e.ctrlKey) {
            step = step * 5;
         } else if (e.shiftKey) {
            step = step * 10;
         }
         let value = (input.value === '') ? 0 : parseFloat(input.value);
         input.value = (value + (step * change));
         this.handleNumberLimits(input);
      }
   }
   handleNumberLimits(input) {
      let [
         min,
         max,
         increase,
         decrease
      ] = [
         input.min,
         input.max,
         input.closest('.quantity')?.querySelector('.increase'),
         input.closest('.quantity')?.querySelector('.decrease')
      ];
      let value = parseFloat(input.value);
      if (value < min) {
         input.value = min;
         decrease.disabled = true;
      } else if (value > max) {
         input.value = max;
         increase.disabled = false;
      } else if (increase.disabled) {
         increase.disabled = false;
      } else if (decrease.disabled) {
         decrease.disabled = false;
      }
   }
   handleChange(event) {
      if (event.target.closest('[data-ignore]') || this.isRestoring) {
         return;
      }
      const target = event.target;
      const form = target.form || target.closest('form');if (!form) return;
      const formConfig = this.forms?.get(form.dataset.formId);
      if (!formConfig) return;
      if (formConfig.options.autosave || this.subscribers.size > 0) {
         // Check conditional fields
         const dependencies = formConfig.dependencies.get(target.name);
         if (dependencies) {
            dependencies.forEach(dep => {
               this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator);
            });
         }
         // Schedule auto-save if enabled
         const delay = this.getDelayForField(target);
         this.scheduleSave(formConfig, delay);
      }
   }
   handleBlur(e) {
      if (e.target.closest('[data-ignore]') || this.isRestoring) {
         return;
      }
      const target = e.target;
      const form = target.form || target.closest('form');
      if (!form) return;
      const input = e.target.closest('input, textarea, select');
      if (input) {
         const fieldWrapper = this.findFieldWrapper(input);
         if (fieldWrapper) {
            // Mark as touched and validate
            const fieldName = fieldWrapper.dataset.field;
            if (fieldName) {
               if (this.shouldDebounce(input)) {
                  window.debouncer.cancel(`validate_${fieldName}`);
               }
               this.touchedFields.add(fieldName);
            }
            this.validateField(input, fieldWrapper);
         }
         const formConfig = this.forms?.get(form.dataset.formId);
         if (formConfig) {
            // Shorter delay on blur
            this.scheduleSave(formConfig, {
               type: 'blur',
               fieldName: target.name,
               delay: 1500
            });
         }
      }
   }
   handleInput(e) {
      if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
         return;
      }
      const input = e.target.closest('input, textarea, select');
      if (!input) return;
      let form = input.closest('form');
      this.showFormStatus(form.dataset.formId, 'pending');
      const fieldWrapper = this.findFieldWrapper(input);
      if (!fieldWrapper) return;
      const fieldName = fieldWrapper.dataset.field;
      if (fieldName) {
         this.touchedFields.add(fieldName);
      }
      if (this.shouldDebounce(input)){
         window.debouncer.schedule(
            `validate_${fieldName}`,
            () => this.validateField.bind(this),
            500
         )
      }
   }
   /***************************************************************
    FORM VALIDATION
   ***************************************************************/
   /**
    * Initialize validation rules
    */
   initValidators() {
      return {
         email: {
            pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: 'Please enter a valid email address'
         },
         url: {
            pattern: /^https?:\/\/.+\..+/,
            message: 'Please enter a valid URL starting with https://'
         },
         phone: {
            pattern: /^[\d\s\-+().]+$/,
            message: 'Please enter a valid phone number'
         },
         number: {
            test: (value, fieldWrapper) => {
               const num = parseFloat(value);
               if (isNaN(num)) return 'Please enter a valid number';
               const min = fieldWrapper.dataset.min;
               const max = fieldWrapper.dataset.max;
               if (min !== undefined && num < parseFloat(min)) {
                  return `Value must be at least ${min}`;
               }
               if (max !== undefined && num > parseFloat(max)) {
                  return `Value must be at most ${max}`;
               }
               return true;
            }
         },
         text: {
            test: (value, fieldWrapper) => {
               const minLength = fieldWrapper.dataset.minlength;
               const maxLength = fieldWrapper.dataset.maxlength;
               if (minLength && value.length < parseInt(minLength)) {
                  return `Must be at least ${minLength} characters`;
               }
               if (maxLength && value.length > parseInt(maxLength)) {
                  return `Must be no more than ${maxLength} characters`;
               }
               return true;
            }
         }
      };
   }
   /**
    * Find the field wrapper (handles both simple and complex fields)
    */
   findFieldWrapper(input) {
      // Try to find the closest .field wrapper
      let wrapper = input.closest('.field');
      // If we're in a repeater row, make sure we get the right field wrapper
      if (!wrapper) {
         wrapper = input.closest('[data-field]');
      }
      return wrapper;
   }
   /**
    * Check if input should be debounced
    */
   shouldDebounce(input) {
      const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
      return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
   }
   /**
    * Validate a single field
    */
   validateField(input, fieldWrapper) {
      const value = this.getFieldValue(input);
      const fieldName = fieldWrapper.dataset.field;
      // Skip validation if field hasn't been touched yet (unless it's required)
      if (!this.touchedFields.has(fieldName) && !input.required) {
         return true;
      }
      // Skip validation if field is empty and not required
      if (!value && !input.required) {
         this.clearValidation(fieldWrapper);
         return true;
      }
      // Check required
      if (input.required && !value) {
         this.showError(fieldWrapper, 'This field is required');
         return false;
      }
      // Check HTML5 validity first
      if (input.checkValidity && !input.checkValidity()) {
         this.showError(fieldWrapper, input.validationMessage);
         return false;
      }
      // Custom pattern validation from data attribute
      const pattern = fieldWrapper.dataset.pattern;
      if (pattern && value) {
         const regex = new RegExp(pattern);
         if (!regex.test(value)) {
            const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
            this.showError(fieldWrapper, message);
            return false;
         }
      }
      // Type-specific validation
      const validateType = fieldWrapper.dataset.validate || input.type;
      if (validateType && this.validators[validateType]) {
         const validator = this.validators[validateType];
         if (validator.pattern && !validator.pattern.test(value)) {
            this.showError(fieldWrapper, validator.message);
            return false;
         }
         if (validator.test) {
            const result = validator.test(value, fieldWrapper);
            if (result !== true) {
               this.showError(fieldWrapper, result);
               return false;
            }
         }
      }
      // All validations passed
      this.showSuccess(fieldWrapper);
      this.notify('field-validated', input);
      return true;
   }
   /**
    * Show success state (green checkmark)
    */
   showSuccess(fieldWrapper, textMessage = '') {
      if (!fieldWrapper) return;
      // Find validation elements (they might be in field-input-wrapper or field-content)
      const success = fieldWrapper.querySelector('.validation-icon.success');
      const error = fieldWrapper.querySelector('.validation-icon.error');
      const message = fieldWrapper.querySelector('.validation-message');
      const input = fieldWrapper.querySelector('input, textarea, select');
      // Remove error state
      fieldWrapper.classList.remove('has-error');
      input?.classList.remove('error');
      // Add success state
      fieldWrapper.classList.add('has-success');
      // Show checkmark (if element exists)
      if (success) {
         success.hidden = false;
      }
      if (error) {
         error.hidden = true;
      }
      // Hide error message
      if (message) {
         if (textMessage === '') {
            message.hidden = true;
            message.textContent = '';
         } else {
            message.hidden = false;
            message.textContent = textMessage;
         }
      }
   }
   /**
    * Show error state (red message below field)
    */
   showError(fieldWrapper, errorMessage) {
      if (!fieldWrapper) return;
      const success = fieldWrapper.querySelector('.validation-icon.success');
      const error = fieldWrapper.querySelector('.validation-icon.error');
      const message = fieldWrapper.querySelector('.validation-message');
      const input = fieldWrapper.querySelector('input, textarea, select');
      // Remove success state
      fieldWrapper.classList.remove('has-success');
      // Add error state
      fieldWrapper.classList.add('has-error');
      input?.classList.add('error');
      // Hide checkmark (if element exists)
      if (success) {
         success.hidden = true;
      }
      //show x
      if (error) {
         error.hidden = false;
      }
      // Show error message
      if (message) {
         message.hidden = false;
         message.textContent = errorMessage;
      }
   }
   /**
    * Clear validation state
    */
   clearValidation(fieldWrapper) {
      if (!fieldWrapper) return;
      const icon = fieldWrapper.querySelector('.validation-icon');
      const message = fieldWrapper.querySelector('.validation-message');
      const input = fieldWrapper.querySelector('input, textarea, select');
      fieldWrapper.classList.remove('has-error', 'has-success');
      input?.classList.remove('error');
      if (icon) {
         icon.hidden = true;
      }
      if (message) {
         message.hidden = true;
         message.textContent = '';
      }
   }
   /* ========== Auto-save functionality ========== */
   /**
    * Get appropriate delay based on field type and context
    */
   getDelayForField(field) {
      // Text fields get longer delay for typing
      if (field.type === 'text' || field.type === 'textarea') {
         return this.autoSaveDefaults.typingDelay;
      }
      // Checkboxes, radios, selects get shorter delay
      if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) {
         return 1000;
      }
      // Default delay
      return this.autoSaveDefaults.delay;
   }
   scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
      if (!formConfig.options.autosave) {
         return;
      }
      document.addEventListener('input', this.saveCheck, {passive: true});
      const saveKey = `autosave_${formConfig.id}`;
      this.debouncer.schedule(
         saveKey,
         () => this.autosave(formConfig),
         delay
      );
   }
   //Extend delay if user is currently typing
   saveCheck(e) {
      let form = e.target.closest('form[data-id]');
      if (!form) {
         return;
      }
      this.scheduleSave(this.forms.get(form.dataset.id));
   }
   async autosave(formConfig) {
      const formData = this.collectFormData(formConfig.element);
      this.showFormStatus(formConfig.id, 'saving');
      // DataStore will now automatically:
      // - Convert Sets/Maps to Arrays/Objects
      // - Strip DOM references
      // - Validate serializability
      await this.store.save({
         formId: formConfig.id,
         data: formData,
         status: 'draft',
         timestamp: Date.now()
      }).then(() => {
         this.showFormStatus(formConfig.id, 'autosaved');
      }).catch(error => {
         console.error('Autosave failed:', error);
         this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
      });
      // Get only changed fields
      const changes = this.getChangedFields(formConfig.data, formData);
      if (Object.keys(changes).length === 0) return;
      // Update stored data
      formConfig.data = formData;
      this.forms.set(formConfig.id, formConfig);
      document.removeEventListener('input', this.handleInput);
      for (let [key, value] of Object.entries(formData)) {
         // Complex fields need full data
         if (typeof value === 'object') {
            changes[key] = value;
         }
      }
      // Notify
      this.notify('form-autosave', {
         formId: formConfig.id,
         changes: changes,
         fullData: formData,
         config: formConfig
      });
   }
   /**
    * Check if form has unsaved changes
    */
   hasUnsavedChanges(formId) {
      const formConfig = this.forms.get(formId);
      if (!formConfig) return false;
      // Check if there are pending operations
      if (formConfig.operations?.size > 0) return true;
      // Check if current data differs from snapshot
      const currentData = this.collectFormData(formConfig.element);
      const changes = this.getChangedFields(formConfig.data, currentData);
      return Object.keys(changes).length > 0;
   }
   showFormStatus(formID, status, message='') {
      let form = this.forms.get(formID);
      if (!form?.options.formStatus) {
         return;
      }
   /**********************************************************************
   STATUS
    **********************************************************************/
   showFormStatus(formId, status, message ='') {
      let form = this.forms.get(formId);
      if (!form || !form.options.showStatus || !form.ui?.status?.status) return;
      if (form.status === status) return;
      if (form.status === status){
         return;
      }
      form.status = status;
      form.ui.status.status.hidden = false;
      form.ui.status.status.classList.toggle('loading', ['uploading', 'saving'].includes(status));
      const statusWrap = form.element.querySelector('.fstatus');
      statusWrap.hidden = false;
      const statusElement = statusWrap.querySelector('.message');
      statusElement.textContent = '';
      statusWrap.querySelector('.icon')?.remove();
      statusWrap.querySelector('.actions')?.remove(); // Clear old actions
      form.ui.status.message.textContent = message === '' ? this.getDefaultMessage(status) : message;
      const messages = {
         'saving': 'Saving changes...',
         'autosaved': 'Changes saved locally. Submit form to send to server.',
         'uploading': 'Uploading your form to server',
         'submitted': 'Successfully sent to server',
         'pending': 'Unsaved changes',
         'restored': 'Welcome back! We\'ve restored your previous entry.',
         'error': 'Failed to save changes. Refresh and try again?',
         'offline': 'Changes will be saved when online'
      };
      const icons = {
         'autosaved': 'check-circle',
         'submitted': 'check-circle',
         'restored': 'history',
         'error': 'close-circle',
         'offline': 'cloud-slash',
         'pending': 'exclamation-mark'
      }
      let icon = window.getIcon(icons[status]);
      if (icon) {
         statusWrap.prepend(icon);
      }
      if (message === '') {
         message = messages[status] || status;
      }
      statusElement.textContent = message;
      statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
      // Add action buttons for certain statuses
      if (status === 'restored') {
         const actions = document.createElement('div');
         actions.className = 'actions';
         actions.innerHTML = `
            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
        `;
         statusWrap.appendChild(actions);
         // Auto-dismiss after 10 seconds
         setTimeout(() => statusWrap.hidden = true, 10000);
      }
      // Auto-hide success messages
      if (status === 'submitted') {
         setTimeout(() => statusWrap.hidden = true, 3000);
      }
      form.ui.status.icon.className = 'icon icon-'+this.getDefaultIcon(status);
      setTimeout(()=> form.ui.status.status.hidden = true, (status === 'submitted') ? 3000 : 10000);
   }
   cleanupSpecialFields() {
      this.specialFields.forEach(field => {
         if (field.type === 'quill' && field.instance) {
            // Remove Quill toolbar
            const toolbar = field.instance.container.previousSibling;
            if (toolbar?.classList.contains('ql-toolbar')) {
               toolbar.remove();
            }
      getDefaultMessage(status) {
         const messages = {
            'saving': 'Saving changes...',
            'autosaved': 'Changes saved locally. Submit form to send to server.',
            'uploading': 'Uploading your form to server',
            'submitted': 'Successfully sent to server',
            'pending': 'Unsaved changes',
            'restored': 'Welcome back! We\'ve restored your previous entry.',
            'error': 'Failed to save changes. Refresh and try again?',
            'offline': 'Changes will be saved when online'
         };
         return messages[status]??status;
      }
      getDefaultIcon(status) {
         const icons = {
            'autosaved': 'check-circle',
            'submitted': 'check-circle',
            'restored': 'history',
            'error': 'close-circle',
            'offline': 'cloud-slash',
            'pending': 'exclamation-mark'
         }
      });
         return icons[status]??'';
      }
      this.uploader?.destroy();
      this.specialFields.clear();
   /**********************************************************************
    SUMMARY
   **********************************************************************/
   showSummary(data) {
      let summary = this.templates.create('formSummary', data);
      data.config.element.after(summary);
      window.fade(data.config.element, false);
   }
   /* ========== Form Data Methods ========== */
   collectFormData(form) {
      if (Object.hasOwn(form.dataset, 'timeline')) {
         return this.collectTimeline(form);
      }
      //Table forms are handled separately
      if (form.classList.contains('table') && form.tagName === 'FORM') {
         return {};
      }
      const formData = new FormData(form);
      let data = {};
      const repeaterData = {};
      const postData = {};
      for (let [key, value] of formData.entries()) {
         if (this.ignore.includes(key) || key.endsWith('_temp')) continue;
         const processor = this.getFieldProcessor(key);
         processor(key, value, data, repeaterData, postData, form);
      }
      if (Object.keys(postData).length !== 0) {
         data = this.mergeRepeaterData(data, repeaterData);
         return this.mergePostData(data, postData);
      }
      return this.mergeRepeaterData(data, repeaterData);
   /**********************************************************************
    UTILITY
   **********************************************************************/
   getForm(element) {
      let form = element.closest('[data-form-id]');
      if (!form) return false;
      let id = form.dataset.formId;
      if (!id) return false;
      let config = this.forms.get(id);
      if (!config) return false;
      return config;
   }
   collectTimeline(form) {
      let data = {};
      let posts = {}; // Temporary object keyed by post ID
      let postOrder = []; // Track order as encountered (preserves DOM/drag order)
      let formData = new FormData(form);
      for (const [key, value] of formData.entries()) {
         if (this.ignore.includes(key) || key.endsWith('_temp')) {
            continue;
         }
         const match = key.match(/^\[(\d+)](.+)$/);
         if (match) {
            // Timeline-specific field: [postId]fieldName
            const [, postId, fieldName] = match;
            if (!posts[postId]) {
               posts[postId] = {
                  id: parseInt(postId),
               };
               postOrder.push(postId); // Track first occurrence
            }
            if (fieldName === 'post_thumbnail') {
               posts[postId]['post_thumbnail'] = parseInt(form.querySelector(`[name="${key}"]`).closest('.item')?.dataset.id);
            } else {
               const processor = this.getFieldProcessor(fieldName);
               processor(fieldName, value, posts[postId], {}, {}, form);
            }
         } else {
            // Shared field (post_title, taxonomies, etc.)
            const processor = this.getFieldProcessor(key);
            processor(key, value, data, {}, {}, form);
         }
      }
      // Convert to array in DOM order (matches menu_order)
      data.timeline = postOrder.map(id => posts[id]);
      delete data['form-id'];
      delete data['sendAll'];
      delete data['timeline_temp'];
      delete data['']; // Empty key
      return data;
   getField(element) {
      return element.closest('[data-field]');
   }
   getFieldProcessor(key) {
      if (key.includes('::')) return this.processGroupField;
      if (key.includes(':')) return this.processRepeaterField;
      if (/\[[^\]]+]/.test(key)) return this.processLocationField;
      return this.processRegularField;
   getFieldType(element) {
      let field = this.getField(element);
      if (!field) return;
      return field.dataset.fieldType;
   }
   getFieldValue(element) {
      let type = this.getFieldType(element);
      let conf = this.getItem(element);
      let fieldName = conf.field?.dataset.field??false;
      if (!fieldName) return false;
   mergeRepeaterData(data, repeaterData) {
      Object.keys(repeaterData).forEach(fieldName => {
         // Clean up empty rows and convert to array format
         const cleanedRows = {};
         Object.keys(repeaterData[fieldName]).forEach(index => {
            const rowData = repeaterData[fieldName][index];
            if (Object.keys(rowData).length > 0) {
               cleanedRows[index] = rowData;
            }
         });
         // Convert to sequential array
         data[fieldName] = Object.values(cleanedRows);
      });
      return data;
   }
   mergePostData(data, postData) {
      for (let [postId, fields] of Object.entries(postData)) {
         data[postId] = fields;
      }
      return data;
   }
   processRepeaterField(key, value, data, repeaterData, postData, form) {
      let [fieldName, index, subField] = key.split(':');
      const isArray = subField.endsWith('[]');
      subField = subField.replace('[]', '');
      //Ensure this repeater and row is in repeaterData
      if (!repeaterData[fieldName]) {
         repeaterData[fieldName] = {};
      }
      if (!repeaterData[fieldName][index]) {
         repeaterData[fieldName][index] = {};
      }
      if (isArray || repeaterData[fieldName][index][subField]) {
         // Initialize as array if not already
         if (!repeaterData[fieldName][index][subField]) {
            repeaterData[fieldName][index][subField] = [];
         } else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
            repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
         }
         repeaterData[fieldName][index][subField].push(value);
      } else {
         // Single value field
         repeaterData[fieldName][index][subField] = value;
      }
   }
   processGroupField(key, value, data, repeaterData, postData, form) {
      const keys = key.split('::');
      const rootGroup = keys[0];
      // Initialize root group if it doesn't exist
      if (!data[rootGroup]) {
         data[rootGroup] = {};
      }
      // Build nested structure step by step
      let current = data[rootGroup];
      for (let i = 1; i < keys.length - 1; i++) {
         const groupKey = keys[i];
         if (!current[groupKey]) {
            current[groupKey] = {};
         }
         current = current[groupKey];
      }
      // Set the final field value
      const fieldKey = keys[keys.length - 1];
      // Handle array values (checkboxes, multi-selects)
      if (current[fieldKey] !== undefined) {
         if (!Array.isArray(current[fieldKey])) {
            current[fieldKey] = [current[fieldKey]];
         }
         current[fieldKey].push(value);
      } else {
         current[fieldKey] = value;
      }
   }
   processLocationField(key, value, data, repeaterData, postData, form) {
      let [fieldKey, v ] = key.split('[');
      v = v.replace(']','');
      if (!Object.hasOwn(data, fieldKey)) {
         data[fieldKey] = {};
         if (!Object.hasOwn(data, 'sendAll')) {
            data['sendAll'] = [fieldKey];
         } else if (!data['sendAll'].includes(fieldKey)) {
            data['sendAll'].push(fieldKey);
         }
      }
      data[fieldKey][v] = value;
   }
   processRegularField(key, value, data, repeaterData, postData, form) {
      //handle array values (like checkboxes/selects)
      key = key.replace('[]','');
      if (data[key]) {
         if (!Array.isArray(data[key])) {
            data[key] = [data[key]];
         }
         data[key].push(value);
      } else {
         data[key] = value;
      }
   }
   /**
    * Get field value (handles different input types)
    */
   getFieldValue(input) {
      if (!input) return '';
      if (input.type === 'checkbox') {
         return input.checked ? input.value || '1' : '';
      } else if (input.type === 'radio') {
         const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
         return checked ? checked.value : '';
      } else if (input.type === 'select-multiple') {
         return Array.from(input.selectedOptions).map(o => o.value);
      }
      return input.value?.trim() || '';
   }
   getChangedFields(original, current) {
      return window.getDifferences?.map(original, current) || {};
   }
   /*******************************************************
    Field Summary
   *******************************************************/
   /**
    * Show a comprehensive summary of form submission
    */
   showSummary(formId, clear = 'form') {
      const formConfig = this.forms.get(formId);
      if (!formConfig) return;
      const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
      const summary = window.getTemplate('formSummary');
      if (!summary) return;
      const wrapper = summary.querySelector('.result');
      // Fields to skip in summary
      const skipFields = ['sendAll', ...this.ignore];
      // Process each field in the form data
      for (const [key, value] of Object.entries(formConfig.data)) {
         // Skip ignored fields and empty values
         if (skipFields.includes(key) || this.isEmptyValue(value)) {
            continue;
         }
         // Get field info from form
         const fieldInfo = this.getFieldInfo(form, key);
         if (!fieldInfo.label) continue; // Skip if no label found
         let field = wrapper.cloneNode(true);
         let title = field.querySelector('h3');
         let p = field.querySelector('p');
         title.textContent = fieldInfo.label;
         let formatted = this.formatFieldValue(value, fieldInfo.type, form);
         if (this.isHtmlContent(formatted)) {
            p.innerHTML = formatted;
         } else {
            p.textContent = formatted;
         }
         summary.append(field);
      }
      let uploads = form.querySelectorAll('[data-upload-field]');
      if (uploads) {
         uploads.forEach(upload => {
            let label = upload.querySelector('h2').textContent;
            let imgs = upload.querySelectorAll('.item-grid.preview img');
            if (imgs) {
               let field = wrapper.cloneNode(true);
               let title = field.querySelector('h3');
               let p = field.querySelector('p');
               p.remove();
               title.textContent = label;
               imgs.forEach(img => {
                  img = img.cloneNode(true);
                  field.append(img);
               });
               summary.append(field);
            }
         });
      }
      // Remove template
      wrapper.remove();
      // Insert summary and hide form
      clear = (clear !== 'form') ? form.closest(clear)??form : form;
      clear.after(summary);
      window.fade(clear, false);
   }
   /**
    * Check if a value is empty (null, undefined, empty string, empty array, empty object)
    */
   isEmptyValue(value) {
      if (value === null || value === undefined || value === '') {
         return true;
      }
      if (Array.isArray(value) && value.length === 0) {
         return true;
      }
      return typeof value === 'object' && Object.keys(value).length === 0;
   }
   /**
    * Get field information (label, type, etc.) from the form
    * Handles special field name patterns ([], ::, :, etc.)
    */
   getFieldInfo(form, fieldName) {
      // Try to find label by 'for' attribute (exact match)
      let label = form.querySelector(`label[for="${fieldName}"]`);
      let input = form.querySelector(`[name=${fieldName}]`);
      // Try to find the input field - check multiple patterns
      if (!input) {
         // Try exact match first
         input = form.querySelector(`[name="${fieldName}"]`);
      }
      if (!input) {
         // Try with [] suffix (for checkboxes, multi-selects)
         input = form.querySelector(`[name="${fieldName}[]"]`);
      }
      if (!input) {
         // Try as fieldset legend (for checkbox/radio groups)
         const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
         if (fieldset) {
            label = fieldset.querySelector('legend');
            input = fieldset.querySelector('input, select, textarea');
         }
      }
      // Get label from input if not found yet
      if (!label && input) {
         // Try closest field wrapper first
         const field = input.closest('.field, fieldset');
         if (field) {
            label = field.querySelector('label, legend, h2');
         }
      }
      // Get field wrapper - always use base name (no special characters)
      let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
      // Determine field type
      let fieldType = 'text';
      if (fieldWrapper?.dataset.type) {
         fieldType = fieldWrapper.dataset.type;
      } else if (input) {
         // Infer from input type
         if (input.type === 'checkbox' && input.name.endsWith('[]')) {
            fieldType = 'checkbox'; // checkbox group
         } else if (input.type === 'checkbox') {
            fieldType = 'true_false'; // single checkbox
         } else if (input.tagName === 'SELECT' && input.multiple) {
            fieldType = 'select'; // multi-select
         } else {
            fieldType = input.type || 'text';
         }
      }
      return {
         label: label?.textContent.replace('*', '').trim() || null,
         type: fieldType,
         wrapper: fieldWrapper,
         input: input
      };
   }
   /**
    * Check if content should be treated as HTML
    */
   isHtmlContent(content) {
      return typeof content === 'string' && (
         content.includes('<br>') ||
         content.includes('<p>') ||
         content.includes('<ul>') ||
         content.includes('<ol>') ||
         content.includes('<a ') ||
         content.includes('<strong>') ||
         content.includes('<em>') ||
         content.includes('<div')
      );
   }
   /**
    * Format field value based on type
    */
   formatFieldValue(value, type, form) {
      switch (type) {
         case 'textarea':
         case 'wysiwyg':
            // Handle rich text - check if it's actual HTML content from Quill
            return this.formatTextareaValue(value, type);
         case 'true_false':
            return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
         case 'checkbox':
            // Handle both single checkbox and checkbox groups
            if (Array.isArray(value)) {
               return this.formatArrayValue(value);
            }
            return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
         case 'select':
            // Handle both single and multi-select
            if (Array.isArray(value)) {
               return this.formatArrayValue(value);
            }
            // Get label from select option
            return this.getSelectLabel(value, form, type);
         case 'date':
         case 'datetime':
         case 'time':
            return window.formatDate ? window.formatDate(value) : value;
         case 'radio':
            // Get label from select option or radio label
            return this.getSelectLabel(value, form, type);
         case 'repeater':
            return this.formatRepeaterValue(value);
            return this.getRepeaterValue(element, conf);
         case 'tag-list':
            return this.getTagListValue(element, conf);
         case 'group':
            return this.formatGroupValue(value);
            //Do we actually need anything here? I think each subfield just
            break;
         case 'location':
            return this.formatLocationValue(value);
            return this.getLocationValue(element, conf);
         case 'selector':
         case 'upload':
            return this.formatFileValue(value);
         case 'gallery':
         case 'image':
            return this.getHiddenInputValue(element, conf, fieldName);
         case 'number':
            return this.formatNumber(value);
         case 'true-false':
         case 'toggle-text':
            return element.checked;
         case 'checkbox':
            // Handle multi-checkbox (name ends with [])
            if (element.name.endsWith('[]')) {
               return this.getCheckboxGroupValue(element, conf);
            }
            return element.checked ? element.value : '';
         default:
            return element.value;
      }
   }
         case 'email':
            return `<a href="mailto:${value}">${value}</a>`;
   /**
    * Get all checked values for a checkbox group
    */
   getCheckboxGroupValue(element, conf) {
      if (!conf.checkboxGroup) {
         conf.checkboxGroup = conf.field?.querySelectorAll(`input[type="checkbox"][name="${element.name}"]`);
         this.saveItem(conf);
      }
         case 'url':
            return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
      return Array.from(conf.checkboxGroup)
         .filter(cb => cb.checked)
         .map(cb => cb.value);
   }
   /**
    * Get the actual user-facing value (for validation and submission)
    */
   getFieldCheckedValue(element) {
      // Handle checkboxes and radios based on checked state
      if (element.type === 'checkbox') {
         const type = this.getFieldType(element);
         if (type === 'true-false') {
            return element.checked;
         }
         return element.checked ? element.value : '';
      }
         case 'phone':
            return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
      if (element.type === 'radio') {
         const radioGroup = document.querySelectorAll(`input[name="${element.name}"]`);
         const checked = Array.from(radioGroup).find(r => r.checked);
         return checked ? checked.value : '';
      }
      // For everything else, use existing logic
      return this.getFieldValue(element);
   }
   isEmptyValue(value) {
      if (value === null || value === undefined || value === '') return true;
      if (Array.isArray(value) && value.length === 0) return true;
      return typeof value === 'object' && Object.keys(value).length === 0;
   }
      getRepeaterValue(element, conf) {
         const items = element.querySelector('.repeater-items');
         if (!items) return [];
         let ignore = ['image_data','image-title','image-caption','image-description','image-alt-text']
         let value = [];
         Array.from(items.children).forEach(row => {
            let rowData = {};
            row.querySelectorAll('[data-field]').forEach(field => {
               if (!ignore.includes(field.dataset.field)) {
                  const input = this.getFieldInput(field);
                  if (input) {
                     rowData[field.dataset.field] = this.getFieldValue(input);
                  }
               }
            });
            value.push(rowData);
         });
         return value;
      }
   getFieldInput(field) {
      // For quill fields, target the specific editor textarea
      const quillTextarea = field.querySelector('textarea[data-editor]');
      if (quillTextarea) return quillTextarea;
      return field.querySelector(this.inputSelectors);
   }
      getTagListValue(element, conf) {
         if (!conf.container) {
            conf.container = conf.field?.querySelector('.tag-items');
            this.saveItem(conf);
         }
         let value = [];
         Array.from(conf.container.children).forEach(item => {
            let inputs = item.querySelectorAll('input[type="hidden"]');
            let fieldData = {};
            inputs.forEach(input => {
               fieldData[input.dataset.field] = input.value;
            });
            value.push(fieldData);
         });
         return value;
      }
      getLocationValue(element, conf) {
         if(!conf.values){
            conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
            this.saveItem(conf);
         }
         let value = {};
         conf.values.forEach(input => {
            value[input.dataset.locationField] = input.value;
         });
         return value;
      }
   getHiddenInputValue(element, conf, fieldName) {
      if (!conf.value) {
         conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`)
            || conf.field?.querySelector(`input[type=hidden]`);
         this.saveItem(conf);
      }
      return conf.value?.value ?? '';
   }
   /**
    * Format field value for display in summary
    * @param {*} value - The field value
    * @param {Object} input - The input config
    * @returns {HTMLElement|string} - Formatted display element or string
    */
   formatValueForSummary(value, input) {
      const fieldType = this.getFieldType(input.element);
      // Handle empty values
      if (this.isEmptyValue(value)) {
         return '';
      }
      // Handle different field types
      switch (fieldType) {
         case 'repeater':
            return this.formatRepeaterForSummary(value, input);
         case 'tag-list':
            return this.formatTagListForSummary(value, input);
         case 'location':
            return this.formatLocationForSummary(value);
         case 'true-false':
            return value ? 'Yes' : 'No';
         case 'checkbox':
            // Handle multi-checkbox arrays
            if (Array.isArray(value)) {
               return this.formatCheckboxGroupForSummary(value, input);
            }
            // Single checkbox - get display label
            return this.getDisplayLabel(input, value);
         case 'selector':
         case 'upload':
         case 'image':  //legacy, shouldn't be needed
         case 'gallery':   //legacy, shouldn't be needed
            // These might need special handling depending on your needs
            return this.formatHiddenFieldForSummary(value, input, fieldType);
         default:
            // Handle arrays (multi-select, checkbox group)
            if (Array.isArray(value)) {
               return this.formatArrayValue(value);
            // For radio/checkbox, get the display label
            if (typeof value === 'string') {
               return this.getDisplayLabel(input, value);
            }
            // For textarea or any multi-line text, convert line breaks
            if (typeof value === 'string' && value.includes('\n')) {
               return this.convertLineBreaks(value);
            }
            return value;
      }
   }
   /**
    * Format repeater field value
    * Format checkbox group values with labels
    */
   formatRepeaterValue(rows) {
      if (!Array.isArray(rows) || rows.length === 0) {
         return '<em>No entries</em>';
      }
   formatCheckboxGroupForSummary(values, input) {
      const labels = values.map(value => this.getDisplayLabel(input, value));
      return labels.join(', ');
   }
      let html = '<div class="repeater-summary">';
   /**
    * Convert \n line breaks to HTML
    */
   convertLineBreaks(text) {
      const container = document.createElement('span');
      container.innerHTML = text.split('\n').join('<br>');
      return container;
   }
   /**
    * Format repeater data as a list
    */
   formatRepeaterForSummary(rows, input) {
      const container = document.createElement('div');
      container.className = 'summary-repeater';
      rows.forEach((row, index) => {
         html += `<div class="repeater-row">`;
         html += `<strong>Entry ${index + 1}:</strong><ul>`;
         for (const [key, value] of Object.entries(row)) {
            if (!this.isEmptyValue(value)) {
               const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
               html += `<li><strong>${label}:</strong> ${value}</li>`;
            }
         const rowDiv = document.createElement('div');
         rowDiv.className = 'summary-repeater-row';
         const rowTitle = document.createElement('strong');
         rowTitle.textContent = `Entry ${index + 1}:`;
         rowDiv.appendChild(rowTitle);
         const fieldsList = document.createElement('ul');
         fieldsList.className = 'summary-repeater-fields';
         for (const [fieldName, fieldValue] of Object.entries(row)) {
            if (this.isEmptyValue(fieldValue)) continue;
            const li = document.createElement('li');
            // Try to find the label for this subfield
            const subFieldElement = input.field?.querySelector(`[data-field="${fieldName}"]`);
            const label = subFieldElement?.closest('.field')?.querySelector('label')?.textContent.replace('*', '').trim() || fieldName;
            li.innerHTML = `<span class="field-label">${label}:</span> <span class="field-value">${fieldValue}</span>`;
            fieldsList.appendChild(li);
         }
         html += `</ul></div>`;
         rowDiv.appendChild(fieldsList);
         container.appendChild(rowDiv);
      });
      html += '</div>';
      return html;
      return container;
   }
   /**
    * Format group field value
    * Format tag-list data
    */
   formatGroupValue(groupData) {
      if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
         return '<em>No data</em>';
      }
   formatTagListForSummary(tags, input) {
      const container = document.createElement('div');
      container.className = 'summary-taglist';
      let html = '<div class="group-summary"><ul>';
      for (const [key, value] of Object.entries(groupData)) {
         if (!this.isEmptyValue(value)) {
            const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
            // Handle nested groups
            if (typeof value === 'object' && !Array.isArray(value)) {
               html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
            } else {
               html += `<li><strong>${label}:</strong> ${value}</li>`;
            }
      const tagsList = document.createElement('ul');
      tagsList.className = 'summary-tags';
      tags.forEach(tag => {
         const li = document.createElement('li');
         li.className = 'summary-tag';
         // Get the primary display value (first non-empty field)
         const displayValue = Object.values(tag).find(v => !this.isEmptyValue(v)) || '';
         // If there are multiple fields, show them all
         const fields = Object.entries(tag).filter(([k, v]) => !this.isEmptyValue(v));
         if (fields.length > 1) {
            li.textContent = fields.map(([k, v]) => v).join(', ');
         } else {
            li.textContent = displayValue;
         }
      }
      html += '</ul></div>';
      return html;
         tagsList.appendChild(li);
      });
      container.appendChild(tagsList);
      return container;
   }
   /**
    * Format location field value
    * Format location data
    */
   formatLocationValue(location) {
      if (typeof location !== 'object') return location;
   formatLocationForSummary(location) {
      const parts = [];
      const fields = ['address', 'city', 'state', 'zip', 'country'];
      fields.forEach(field => {
         if (location[field]) {
            parts.push(location[field]);
         }
      });
      if (location.street) parts.push(location.street);
      if (location.city) parts.push(location.city);
      if (location.province) parts.push(location.province);
      if (location.postal_code) parts.push(location.postal_code);
      if (location.country) parts.push(location.country);
      return parts.join(', ');
      return parts.length > 0 ? parts.join(', ') : location.address || '';
   }
   /**
    * Format file/image value
    * Format hidden field types (upload, selector)
    */
   formatFileValue(value) {
      if (typeof value === 'string') {
         // Single file - could be URL or filename
         if (value.startsWith('http')) {
            return `<a href="${value}" target="_blank">View file</a>`;
   formatHiddenFieldForSummary(value, input, fieldType) {
      if (['upload', 'gallery', 'image'].includes(fieldType)) {
         // Get upload preview images if available
         const uploadField = input.field?.querySelector('[data-upload-field]');
         if (uploadField) {
            const previews = uploadField.querySelectorAll('.item-grid.preview img');
            if (previews.length > 0) {
               const container = document.createElement('div');
               container.className = 'summary-uploads';
               previews.forEach(img => {
                  const clone = img.cloneNode(true);
                  clone.style.maxWidth = '100px';
                  clone.style.maxHeight = '100px';
                  container.appendChild(clone);
               });
               return container;
            }
         }
         return `${value.split(',').length} file(s) uploaded`;
      }
      if (fieldType === 'selector') {
         // Could enhance this to show selected item names if available
         return value;
      }
      if (Array.isArray(value)) {
         return value.map(file => {
            if (typeof file === 'string') {
               return `<a href="${file}" target="_blank">View file</a>`;
            }
            return file.name || 'File';
         }).join(', ');
      }
      return 'File uploaded';
   }
   /**
    * Format number with proper locale formatting
    */
   formatNumber(value) {
      const num = parseFloat(value);
      if (isNaN(num)) return value;
      // Check if it's likely currency (has 2 decimal places)
      if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
         return new Intl.NumberFormat('en-CA', {
            style: 'currency',
            currency: 'USD'
         }).format(num);
      }
      return new Intl.NumberFormat('en-CA').format(num);
   }
   /**
    * Format array values (checkboxes, multi-select)
    */
   /**
    * Format array values (checkboxes, multi-select)
    */
   formatArrayValue(arr, form = null, fieldInfo = null) {
      if (arr.length === 0) return '<em>None selected</em>';
      // If we have field info, try to get proper labels
      if (form && fieldInfo && fieldInfo.input) {
         const labeled = arr.map(val => {
            return this.getSelectLabel(val, form, fieldInfo.type);
         });
         return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
      }
      // Fallback to raw values
      return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
   }
   /**
    * Get label for select/radio option
    */
   /**
    * Get label for select/radio/checkbox option
    */
   getSelectLabel(value, form, type) {
      if (type === 'select') {
         const option = form.querySelector(`option[value="${value}"]`);
         return option?.textContent || value;
      }
      if (type === 'radio') {
         const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
         const label = radio?.nextElementSibling;
         return label?.textContent || value;
      }
      if (type === 'checkbox') {
         // Try to find the checkbox with this value
         const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
         if (checkbox) {
            // Look for associated label
            const label = form.querySelector(`label[for="${checkbox.id}"]`);
            if (label) {
               return label.textContent.trim();
            }
            // Try next sibling
            const nextLabel = checkbox.nextElementSibling;
            if (nextLabel?.tagName === 'LABEL') {
               return nextLabel.textContent.trim();
            }
         }
      }
      return value;
   }
   /**
    * Format textarea value - handles both rich text and plain text
    * Get the display label for an input value (especially for radio/checkbox)
    * @param {Object} input - The input config from this.inputs
    * @param {*} value - The field value
    * @returns {string} - The display label or original value
    */
   formatTextareaValue(value, type) {
      if (!value) return '<em>Empty</em>';
   getDisplayLabel(input, value) {
      if (!input.element) return value;
      // If it's explicitly a wysiwyg type or contains HTML tags, use as-is
      if (type === 'wysiwyg' || this.containsHtml(value)) {
         // Quill content already has proper HTML structure
         return value;
      const inputType = input.element.type;
      // Handle radio buttons
      if (inputType === 'radio') {
         const radioGroup = input.field.querySelectorAll(`input[type="radio"][name="${input.element.name}"]`);
         const selectedRadio = Array.from(radioGroup).find(radio => radio.value === value);
         if (selectedRadio) {
            const label = selectedRadio.closest('label') ||
               input.field.querySelector(`label[for="${selectedRadio.id}"]`);
            if (label) {
               return label.textContent.replace('*', '').trim();
            }
         }
      }
      // Plain textarea - preserve formatting
      return this.formatPlainText(value);
   }
   /**
    * Check if string contains HTML content (more reliable than just checking for '<')
    */
   containsHtml(str) {
      // Check for common HTML tags that Quill uses
      const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
      return htmlPattern.test(str);
   }
   /**
    * Format plain text content - preserves whitespace and converts newlines
    */
   formatPlainText(text) {
      if (!text) return '';
      // First, escape any HTML entities that might be in the text
      text = text
         .replace(/&/g, '&amp;')
         .replace(/</g, '&lt;')
         .replace(/>/g, '&gt;');
      // Convert double newlines to paragraphs for better readability
      const paragraphs = text.split(/\n\n+/);
      if (paragraphs.length > 1) {
         // Multiple paragraphs
         return paragraphs
            .map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
            .join('');
      // Handle checkboxes (including groups)
      if (inputType === 'checkbox' && this.getFieldType(input.element) !== 'true-false') {
         // Find checkbox with this value in the field
         const checkbox = input.field.querySelector(`input[type="checkbox"][value="${value}"]`);
         if (checkbox) {
            const label = checkbox.closest('label') ||
               input.field.querySelector(`label[for="${checkbox.id}"]`);
            if (label) {
               // Get just the span content to avoid getting nested elements
               const span = label.querySelector('span');
               return span ? span.textContent.trim() : label.textContent.replace('*', '').trim();
            }
         }
      }
      // Single paragraph - just convert newlines to breaks
      return text.replace(/\n/g, '<br>');
      return value;
   }
   getItem(element, formId = null) {
      const hasID = Object.hasOwn(element.dataset, 'ref');
      let id = (hasID) ? element.dataset.ref : window.generateID('input');
      if (!hasID) element.dataset.ref = id;
   /**
    * Event system
    */
      //check if we have it already
      if (!this.inputs.has(id)) {
         if (!formId) {
            formId = element.closest('[data-form-id]')?.dataset.formId??false;
         }
         let field = this.getField(element);
         this.inputs.set(id, {
            id: id,
            element: element,
            form: formId,
            field: field,
            section: element.closest('[data-tab]')?.dataset.tab ?? false,
            ui: window.uiFromSelectors(this.selectors.fields, field)
         });
      }
      return this.inputs.get(id);
   }
   saveItem(config) {
      this.inputs.set(config.id, config);
   }
   /**********************************************************************
    Subscription
   **********************************************************************/
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   notify(event, data) {
      this.subscribers.forEach(cb => cb(event, data));
   }
   /**
    * Cleanup when form is closed/destroyed
    */
   cleanupForm(formId) {
      const formConfig = this.forms.get(formId);
      if (!formConfig) return;
      // Check for unsaved changes
      if (this.hasUnsavedChanges(formId)) {
         this.autosave(formConfig);
      }
      // Clean up special fields
      this.cleanupSpecialFields();
      // Remove form config
      this.forms.delete(formId);
   }
   /**
    * Cleanup
    */
   destroy() {
      // Remove global handlers
      if (this.globalHandlersAdded) {
         document.removeEventListener('change', this.changeHandler);
         document.removeEventListener('blur', this.blurHandler, true);
         document.removeEventListener('input', this.inputHandler, true);
      }
      this.forms.forEach((formConfig) => {
         let element = formConfig.element;
         if (element) {
            element.removeEventListener('submit', this.submitHandler);
      this.subscribers.forEach(cb => {
         try {
            cb(event, data);
         } catch (e) {
            console.error('HandleSelection subscriber error:', e);
         }
      });
      // Clear maps
      this.specialFields.clear();
      this.forms.clear();
      this.activeRepeaters.clear();
      if (this.forms) {
   }
   /**********************************************************************
    Cleanup
   **********************************************************************/
   destroy() {
      if (this.forms.size > 0) {
         Array.from(this.forms.values()).forEach(form => {
            this.removeFormListeners(form);
         });
         this.forms.clear();
      }
      if (this.repeaters.size > 0) {
         Array.from(this.repeaters.values()).forEach(repeater => {
            this.removeRepeaterListeners(repeater.element);
            repeater.sortable?.destroy();
         });
         this.repeaters.clear();
      }
      if (this.quantityFields.size > 0) {
         Array.from(this.quantityFields.values()).forEach(num => {
            this.removeQuantityListeners(num.element);
         });
         this.quantityFields.clear();
      }
      if (this.tagLists.size > 0) {
         Array.from(this.tagLists.values()).forEach(tagList => {
            this.removeTagListListeners(tagList.element);
         });
         this.tagLists.clear();
      }
      if (this.charLimits.size > 0) {
         Array.from(this.charLimits.values()).forEach(charLimit => {
            charLimit.element.removeEventListener('input', this.countUpdaters);
         });
      }
      this.inputs.clear();
      this.forms.clear();
      this.charLimits.clear();
   }
}
document.addEventListener('DOMContentLoaded', async function () {
   window.auth.subscribe(event => {
      if (event === 'auth-loaded') {
         window.jvbForm = FormController;
         window.jvbForm = new FormController();
      }
   });
});