Jake Vanderwerf
2026-01-04 eea4e21d9bd7b89f7124fa1acbe3347d68db6d90
assets/js/concise/PopulateForm.js
@@ -1,25 +1,78 @@
/**********************************************
 PopulateForm extracts saved data and populates the form field accordingly
**********************************************/
 **********************************************/
class PopulateForm {
   constructor(form, fieldData = {}, imageData = {}, options = {}) {
      for (let [fieldName, fieldValue] of Object.entries(fieldData)) {
   constructor(form, itemDataOrFields = {}, legacyImages = {}, options = {}) {
      // Support both old signature (fields, images) and new signature (item object)
      this.item = this.normalizeItemData(itemDataOrFields, legacyImages);
      this.form = form;
      this.options = options;
      // Populate all fields
      for (let [fieldName, fieldValue] of Object.entries(this.item.fields)) {
         let wrapper = form.querySelector(`[data-field="${fieldName}"]`);
         if (wrapper) {
            this.populateField(wrapper, fieldName, fieldValue, imageData, options);
            this.populateField(wrapper, fieldName, fieldValue);
         }
      }
   }
   /**
    * Populate a single field with its value
    * @param {HTMLElement} fieldWrapper - The field wrapper element
    * @param {string} fieldName - Field name
    * @param {*} fieldValue - Field value
    * @param {Object} imagesData - Image metadata
    * @param {Object} options - Additional options
    * Normalize data to consistent structure
    * Supports both new format (item object) and legacy format (fields, images)
    */
   populateField(fieldWrapper, fieldName, fieldValue, imagesData = {}, options = {}) {
   normalizeItemData(itemDataOrFields, legacyImages) {
      // Check if this is the new format (has a fields property) or legacy format
      if (itemDataOrFields && typeof itemDataOrFields === 'object' && 'fields' in itemDataOrFields) {
         // New format - already structured
         return {
            fields: itemDataOrFields.fields || {},
            images: itemDataOrFields.images || {},
            taxonomies: itemDataOrFields.taxonomies || {}
         };
      } else {
         // Legacy format - fields and images passed separately
         return {
            fields: itemDataOrFields || {},
            images: legacyImages || {},
            taxonomies: {}
         };
      }
   }
   /**
    * Check if a field is a taxonomy field
    */
   isTaxonomyField(fieldName) {
      return Object.hasOwn(this.item.taxonomies, fieldName) &&
         Object.keys(this.item.taxonomies[fieldName]).length > 0;
   }
   /**
    * Check if a value references image data
    */
   isImageField(value) {
      if (!this.item.images || Object.keys(this.item.images).length === 0) {
         return false;
      }
      const ids = this.splitIDs(value);
      return ids.some(id => Object.keys(this.item.images).includes(String(id)));
   }
   /**
    * Split comma-separated IDs into array of integers
    */
   splitIDs(value) {
      return String(value).split(',')
         .map(v => parseInt(v.trim()))
         .filter(v => !isNaN(v) && v > 0);
   }
   /**
    * Populate a single field with its value
    */
   populateField(fieldWrapper, fieldName, fieldValue, options = {}) {
      if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
         return;
      }
@@ -28,16 +81,14 @@
      const fieldType = this.getFieldType(fieldWrapper);
      switch (fieldType) {
         case 'image':
            this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
            break;
         case 'upload':
         case 'gallery':
            this.populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData);
         case 'image':
            this.populateUploadField(fieldWrapper, fieldName, fieldValue);
            break;
         case 'repeater':
            this.populateRepeaterField(fieldWrapper, fieldName, fieldValue, options);
            this.populateRepeaterField(fieldWrapper, fieldName, fieldValue);
            break;
         case 'taxonomy':
@@ -75,6 +126,7 @@
         case 'number':
            this.populateNumberField(fieldWrapper, fieldName, fieldValue);
            break;
         case 'textarea':
            if (fieldWrapper.querySelector('.editor-container')) {
               this.populateEditorField(fieldWrapper, fieldName, fieldValue);
@@ -95,14 +147,317 @@
   }
   /**
    * Determine field type from wrapper element
    * @param {HTMLElement} fieldWrapper - Field wrapper element
    * @returns {string} Field type
    * Populate taxonomy fields with visual display
    */
   populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
      // Handle different value formats
      let termIds = [];
      if (Array.isArray(fieldValue)) {
         termIds = fieldValue.map(v => String(v));
      } else if (typeof fieldValue === 'string') {
         try {
            const parsed = JSON.parse(fieldValue);
            termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
         } catch (e) {
            termIds = fieldValue.split(',').map(v => v.trim()).filter(v => v);
         }
      } else if (fieldValue) {
         termIds = [String(fieldValue)];
      }
      if (termIds.length === 0) {
         return;
      }
      // Update hidden input
      const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
      if (hiddenInput) {
         hiddenInput.value = termIds.join(',');
         // Trigger TaxonomySelector to update visual display
         const toggle = fieldWrapper.querySelector('.taxonomy-toggle');
         if (toggle && toggle.dataset.fieldId && window.jvbTaxonomy) {
            // Use requestAnimationFrame to ensure DOM is ready
            requestAnimationFrame(() => {
               window.jvbTaxonomy.updateFieldFromInput(toggle.dataset.fieldId);
            });
         }
      }
   }
   /**
    * Populate upload fields (images, videos, files)
    */
   populateUploadField(fieldWrapper, fieldName, fieldValue) {
      // Check if this is a timeline gallery
      const isTimeline = fieldWrapper.dataset.subtype === 'timeline' || fieldName === 'timeline';
      if (isTimeline) {
         this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue);
         return;
      }
      if (!fieldValue) {
         return;
      }
      // Handle comma-separated IDs or single ID
      const itemIds = this.splitIDs(fieldValue);
      if (itemIds.length === 0) {
         return;
      }
      // Update hidden input
      const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
      if (hiddenInput) {
         hiddenInput.value = itemIds.join(',');
      }
      // Update display grid
      const grid = fieldWrapper.querySelector('.item-grid');
      const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
      // Clear existing items first
      if (grid) {
         window.removeChildren(grid);
      }
      fieldWrapper.querySelector('.progress')?.remove();
      if (grid) {
         itemIds.forEach(itemId => {
            const template = window.getTemplate('uploadItem');
            if (!template) {
               console.warn('uploadItem template not found');
               return;
            }
            this.populateUploadItem(template, itemId);
            grid.append(template);
         });
         // Hide upload container if items exist
         if (itemIds.length > 0 && uploadContainer) {
            uploadContainer.hidden = true;
         }
      }
   }
   /**
    * Populate a single upload item
    */
   populateUploadItem(template, itemId) {
      let input = template.querySelector('input[name="select-item"]');
      let label = template.querySelector('label[for="select-item"]');
      template.dataset.id = itemId;
      input.name = `select-item-${itemId}`;
      input.id = input.name;
      label.htmlFor = input.name;
      const img = template.querySelector('img');
      template.querySelector('video')?.remove();
      // Populate with data from item.images
      if (this.item.images[itemId]) {
         const data = this.item.images[itemId];
         if (img) {
            img.src = data.medium || data.small || data.large || '';
            img.alt = data['image-alt-text'] || data.alt || '';
         }
         // Populate metadata fields
         const titleInput = template.querySelector('[name="image-title"]');
         const altInput = template.querySelector('[name="image-alt-text"]');
         const captionInput = template.querySelector('[name="image-caption"]');
         if (titleInput) titleInput.value = data['image-title'] || data.title || '';
         if (altInput) altInput.value = data['image-alt-text'] || data.alt || '';
         if (captionInput) captionInput.value = data['image-caption'] || data.caption || '';
      } else {
         console.warn(`No image data found for ID: ${itemId}`);
      }
      // Remove hint if present
      template.querySelector('details .upload-meta > .hint')?.remove();
   }
   /**
    * Populate timeline gallery - FIXED iteration
    */
   populateTimelineGallery(fieldWrapper, fieldName, fieldValue) {
      console.log('Populating Timeline Gallery', fieldValue);
      if (!fieldValue || !Array.isArray(fieldValue)) {
         console.warn('Timeline field value must be an array');
         return;
      }
      if (fieldValue.length === 0) {
         return;
      }
      const grid = fieldWrapper.querySelector('.item-grid');
      const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
      // Clear existing items
      if (grid) {
         window.removeChildren(grid);
      }
      fieldWrapper.querySelector('.progress')?.remove();
      if (!grid) return;
      // FIX: Iterate directly over array, not Object.entries
      for (let itemData of fieldValue) {
         const template = window.getTemplate('timelineItem');
         if (!template) {
            console.warn('timelineItem template not found');
            continue;
         }
         const imageId = itemData.post_thumbnail;
         const postId = itemData.id;
         // Set template data attributes
         template.dataset.id = imageId;
         template.dataset.postId = postId;
         // Update selection controls
         let input = template.querySelector('input[name="select-item"]');
         let label = template.querySelector('label[for="select-item"]');
         if (input && label) {
            input.name = `select-item-${imageId}`;
            input.id = input.name;
            label.htmlFor = input.name;
         }
         // Remove unnecessary elements
         template.querySelector('video')?.remove();
         template.querySelector('.select-item span')?.remove();
         // Populate main image
         const img = template.querySelector('img');
         const imgData = this.item.images[imageId];
         if (img && imgData) {
            img.src = imgData.medium || imgData.small || imgData.large || '';
            img.title = imgData['image-title'] || '';
            img.alt = imgData['image-alt-text'] || '';
         }
         // Populate all fields within the template
         const fields = template.querySelectorAll('.field');
         fields.forEach(field => {
            if (field.classList.contains('group')) {
               return;
            }
            const input = field.querySelector('input:not([type="file"]), textarea');
            if (!input) return;
            const label = field.querySelector('label');
            const fieldName = input.name.replace('upload_data::', '').replace(/^\[.*?\]/, '');
            // Get value from itemData or imgData
            let value = itemData[fieldName];
            if (value === undefined && imgData) {
               value = imgData[fieldName];
            }
            // Populate the field using our standard method
            if (value !== undefined && value !== null) {
               this.populateField(field, fieldName, value);
            }
            // Update field identifiers to include post ID
            const newName = `[${postId}]${fieldName}`;
            const newId = newName;
            input.name = newName;
            input.id = newId;
            if (label) label.htmlFor = newId;
         });
         grid.append(template);
      }
      // Hide upload container if items exist
      if (fieldValue.length > 0 && uploadContainer) {
         uploadContainer.hidden = true;
      }
   }
   populateTextField(fieldWrapper, fieldName, fieldValue) {
      const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
      if (input && input.type !== 'file') {
         input.value = String(fieldValue || '');
         if (input.dataset.limit) {
            const counter = fieldWrapper.querySelector('.char-count .current');
            if (counter) {
               counter.textContent = input.value.length;
            }
         }
      }
   }
   populateTextareaField(fieldWrapper, fieldName, fieldValue) {
      const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
         fieldWrapper.querySelector('textarea:not([data-editor="true"])');
      if (textarea) {
         textarea.value = String(fieldValue || '');
         textarea.dispatchEvent(new Event('change', { bubbles: true }));
         if (textarea.dataset.limit) {
            const counter = fieldWrapper.querySelector('.char-count .current');
            if (counter) {
               counter.textContent = textarea.value.length;
               const limit = parseInt(textarea.dataset.limit, 10);
               fieldWrapper.classList.toggle('reached', textarea.value.length >= limit);
            }
         }
      }
   }
   populateEditorField(fieldWrapper, fieldName, fieldValue) {
      const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"][data-editor="true"]`);
      if (!textarea) return;
      textarea.value = String(fieldValue || '');
      const editorContainer = fieldWrapper.querySelector('.editor');
      const content = fieldValue || '<p><br></p>';
      if (editorContainer) {
         let quillInstance = editorContainer.__quill;
         if (!quillInstance && window.Quill) {
            for (let instance of (window.Quill.instances || [])) {
               if (instance.container === editorContainer) {
                  quillInstance = instance;
                  break;
               }
            }
         }
         if (quillInstance) {
            quillInstance.root.innerHTML = content;
            editorContainer.__quill = quillInstance;
         } else {
            editorContainer.innerHTML = content;
         }
      }
      textarea.dispatchEvent(new Event('change', { bubbles: true }));
   }
   getFieldType(fieldWrapper) {
      // Check for specific field classes
      if (fieldWrapper.dataset.fieldType) return fieldWrapper.dataset.fieldType;
      if (fieldWrapper.dataset.type) return fieldWrapper.dataset.type;
      const typeClasses = [
         'image', 'gallery', 'repeater', 'taxonomy', 'user', 'location',
         'upload', 'repeater', 'taxonomy', 'user', 'location',
         'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
         'time', 'datetime', 'editor', 'number', 'text', 'textarea',
         'email', 'url', 'tel', 'phone'
@@ -114,12 +469,6 @@
         }
      }
      // Check for data attribute
      if (fieldWrapper.dataset.type) {
         return fieldWrapper.dataset.type;
      }
      // Check input type
      const input = fieldWrapper.querySelector('input, select, textarea');
      if (input) {
         if (input.tagName === 'TEXTAREA') {
@@ -134,60 +483,6 @@
   }
   /**
    * Populate text-based fields
    */
   populateTextField(fieldWrapper, fieldName, fieldValue) {
      const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
      if (input) {
         input.value = String(fieldValue || '');
         // Update character counter if present
         if (input.dataset.limit) {
            const counter = fieldWrapper.querySelector('.char-count .current');
            if (counter) {
               counter.textContent = input.value.length;
            }
         }
      }
   }
   /**
    * Populate textarea fields
    */
   populateTextareaField(fieldWrapper, fieldName, fieldValue) {
      const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
         fieldWrapper.querySelector('textarea:not([data-editor="true"])');
      if (textarea) {
         const oldValue = textarea.value;
         textarea.value = String(fieldValue || '');
         // Trigger change event to update any dependencies
         textarea.dispatchEvent(new Event('change', { bubbles: true }));
         // Update character counter if present
         if (textarea.dataset.limit) {
            const counter = fieldWrapper.querySelector('.char-count .current');
            if (counter) {
               counter.textContent = textarea.value.length;
               // Check if limit is reached
               const limit = parseInt(textarea.dataset.limit, 10);
               if (textarea.value.length >= limit) {
                  fieldWrapper.classList.add('reached');
               } else {
                  fieldWrapper.classList.remove('reached');
               }
            }
         }
      } else {
         console.warn(`No textarea found for field ${fieldName} in wrapper:`, fieldWrapper);
      }
   }
   /**
    * Populate number fields
    */
   populateNumberField(fieldWrapper, fieldName, fieldValue) {
@@ -288,71 +583,6 @@
   }
   /**
    * Populate editor fields (Quill)
    */
   populateEditorField(fieldWrapper, fieldName, fieldValue) {
      const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
         fieldWrapper.querySelector('textarea[data-editor="true"]') ||
         fieldWrapper.querySelector('textarea');
      if (!textarea) {
         console.warn(`Editor field ${fieldName}: textarea not found`);
         return;
      }
      const content = String(fieldValue || '');
      // Update the textarea value
      textarea.value = content;
      // Try to find and update Quill editor
      const editorContainer = fieldWrapper.querySelector('.editor');
      if (editorContainer) {
         // Try different ways to access the Quill instance
         let quillInstance = null;
         // Method 1: Check if Quill is stored on the editor element
         if (editorContainer.__quill) {
            quillInstance = editorContainer.__quill;
         }
         // Method 2: Check if Quill is stored as quill property
         else if (editorContainer.quill) {
            quillInstance = editorContainer.quill;
         }
         // Method 3: Try to find Quill in the global registry (if you have one)
         else if (window.Quill && window.Quill.find) {
            quillInstance = window.Quill.find(editorContainer);
         }
         // Method 4: Check all Quill instances if available
         else if (window.Quill && window.Quill.instances) {
            // Some setups store instances in a registry
            for (let instance of window.Quill.instances) {
               if (instance.container === editorContainer) {
                  quillInstance = instance;
                  break;
               }
            }
         }
         if (quillInstance) {
            // Set the content in Quill
            quillInstance.root.innerHTML = content;
            // Store the instance reference for future use
            editorContainer.__quill = quillInstance;
         } else {
            console.warn(`Quill instance not found for ${fieldName}, setting HTML directly`);
            // Fallback: set HTML directly
            editorContainer.innerHTML = content;
         }
      } else {
         console.warn(`Editor container not found for ${fieldName}`);
      }
      // Trigger change event on textarea
      textarea.dispatchEvent(new Event('change', { bubbles: true }));
   }
   /**
    * Populate location fields
    */
   populateLocationField(fieldWrapper, fieldName, fieldValue) {
@@ -373,36 +603,6 @@
      });
   }
   /**
    * Populate taxonomy fields
    */
   populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
      // Handle different value formats
      let termIds = [];
      if (Array.isArray(fieldValue)) {
         termIds = fieldValue.map(v => String(v));
      } else if (typeof fieldValue === 'string') {
         try {
            const parsed = JSON.parse(fieldValue);
            termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
         } catch (e) {
            termIds = fieldValue.split(',').map(v => v.trim());
         }
      } else if (fieldValue) {
         termIds = [String(fieldValue)];
      }
      if (termIds.length === 0) {
         return;
      }
      // Update hidden input
      const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
      if (hiddenInput) {
         hiddenInput.value = termIds.join(',');
      }
   }
   /**
    * Populate user fields (similar to taxonomy)
@@ -413,76 +613,9 @@
   }
   /**
    * Populate image fields
    */
   populateImageField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
      if (!fieldValue) {
         return;
      }
      // Handle comma-separated IDs or single ID
      const imageIds = String(fieldValue).split(',').filter(id => parseInt(id.trim()));
      if (imageIds.length === 0) {
         return;
      }
      // Update hidden input
      const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
      if (hiddenInput) {
         hiddenInput.value = imageIds.join(',');
      }
      // Update image display
      const grid = fieldWrapper.querySelector('.item-grid');
      const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
      if (grid) {
         window.removeChildren(grid);
         imageIds.forEach(imageId => {
            let image = window.getTemplate('uploadItem');
            let img = image.querySelector('img');
            let details = image.querySelector('details');
            let meta = window.getTemplate('uploadMeta')
            details.append(meta);
            [
               img.src,
               img.alt,
               image.querySelector('[name="image-title"]').value,
               image.querySelector('[name="image-alt-text"]').value,
               image.querySelector('[name="image-caption"]').value,
            ] = [
               imagesData[imageId].medium,
               imagesData[imageId].alt,
               imagesData[imageId].title,
               imagesData[imageId].alt,
               imagesData[imageId].caption,
            ];
            details.querySelector('.upload-meta > .hint')?.remove();
            grid.append(image);
         });
         if (imageIds.length > 0) {
            if (uploadContainer) {
               uploadContainer.hidden = true;
            }
         }
      }
   }
   /**
    * Populate gallery fields
    */
   populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
      this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
   }
   /**
    * Populate repeater fields
    */
   populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) {
   populateRepeaterField(fieldWrapper, fieldName, fieldValue) {
      if (!fieldValue || !Array.isArray(fieldValue)) {
         return;
      }
@@ -566,4 +699,5 @@
   }
}
// Make available globally
window.jvbPopulate = PopulateForm;