/********************************************** PopulateForm extracts saved data and populates the form field accordingly **********************************************/ class PopulateForm { 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); } } } /** * Normalize data to consistent structure * Supports both new format (item object) and legacy format (fields, images) */ 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; } // Determine field type from classes or data attributes const fieldType = this.getFieldType(fieldWrapper); switch (fieldType) { case 'upload': case 'gallery': case 'image': this.populateUploadField(fieldWrapper, fieldName, fieldValue); break; case 'repeater': this.populateRepeaterField(fieldWrapper, fieldName, fieldValue); break; case 'taxonomy': this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue); break; case 'user': this.populateUserField(fieldWrapper, fieldName, fieldValue); break; case 'location': this.populateLocationField(fieldWrapper, fieldName, fieldValue); break; case 'set': case 'checkbox': this.populateSetField(fieldWrapper, fieldName, fieldValue); break; case 'select': case 'radio': this.populateSelectField(fieldWrapper, fieldName, fieldValue); break; case 'true_false': this.populateBooleanField(fieldWrapper, fieldName, fieldValue); break; case 'date': case 'time': case 'datetime': this.populateDateField(fieldWrapper, fieldName, fieldValue); break; case 'number': this.populateNumberField(fieldWrapper, fieldName, fieldValue); break; case 'textarea': if (fieldWrapper.querySelector('.editor-container')) { this.populateEditorField(fieldWrapper, fieldName, fieldValue); } else { this.populateTextareaField(fieldWrapper, fieldName, fieldValue); } break; case 'text': case 'email': case 'url': case 'tel': case 'phone': default: this.populateTextField(fieldWrapper, fieldName, fieldValue); break; } } /** * 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 || '


'; 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) { if (fieldWrapper.dataset.fieldType) return fieldWrapper.dataset.fieldType; if (fieldWrapper.dataset.type) return fieldWrapper.dataset.type; const typeClasses = [ 'upload', 'repeater', 'taxonomy', 'user', 'location', 'set', 'checkbox', 'select', 'radio', 'true_false', 'date', 'time', 'datetime', 'editor', 'number', 'text', 'textarea', 'email', 'url', 'tel', 'phone' ]; for (const type of typeClasses) { if (fieldWrapper.classList.contains(type)) { return type; } } const input = fieldWrapper.querySelector('input, select, textarea'); if (input) { if (input.tagName === 'TEXTAREA') { return input.dataset.editor === 'true' ? 'editor' : 'textarea'; } if (input.type) { return input.type === 'checkbox' && !fieldWrapper.classList.contains('true_false') ? 'set' : input.type; } } return 'text'; } /** * Populate number fields */ populateNumberField(fieldWrapper, fieldName, fieldValue) { const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="number"]`); if (input) { input.value = Number(fieldValue) || 0; } } /** * Populate boolean/true_false fields */ populateBooleanField(fieldWrapper, fieldName, fieldValue) { const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="checkbox"]`); if (input) { input.checked = Boolean(fieldValue); } } /** * Populate select/radio fields */ populateSelectField(fieldWrapper, fieldName, fieldValue) { const value = String(fieldValue || ''); // Try select first const select = fieldWrapper.querySelector(`select[name="${fieldName}"]`); if (select) { select.value = value; return; } // Try radio buttons const radio = fieldWrapper.querySelector(`input[type="radio"][name="${fieldName}"][value="${value}"]`); if (radio) { radio.checked = true; } } /** * Populate set/checkbox fields (multiple selections) */ populateSetField(fieldWrapper, fieldName, fieldValue) { // Parse value if it's a string let values = fieldValue; if (typeof fieldValue === 'string') { try { values = JSON.parse(fieldValue); } catch (e) { values = fieldValue.split(',').map(v => v.trim()); } } if (!Array.isArray(values)) { values = [String(values)]; } // Update checkboxes fieldWrapper.querySelectorAll(`input[type="checkbox"][name*="${fieldName}"]`).forEach(checkbox => { checkbox.checked = values.includes(checkbox.value); }); } /** * Populate date/time fields */ populateDateField(fieldWrapper, fieldName, fieldValue) { const input = fieldWrapper.querySelector(`[name="${fieldName}"], input`); if (input && fieldValue) { // Handle different date formats let dateValue = fieldValue; if (typeof fieldValue === 'object' && fieldValue.date) { dateValue = fieldValue.date; } // Convert to appropriate format for input type try { const date = new Date(dateValue); if (!isNaN(date.getTime())) { switch (input.type) { case 'date': input.value = date.toISOString().split('T')[0]; break; case 'time': input.value = date.toTimeString().slice(0, 5); break; case 'datetime-local': input.value = date.toISOString().slice(0, 16); break; default: input.value = dateValue; } } } catch (e) { input.value = dateValue; } } } /** * Populate location fields */ populateLocationField(fieldWrapper, fieldName, fieldValue) { if (!fieldValue || typeof fieldValue !== 'object') { return; } // Location fields typically have sub-fields const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country']; subFields.forEach(subField => { if (fieldValue[subField] !== undefined) { const input = fieldWrapper.querySelector(`[name="${fieldName}_${subField}"], [name="${subField}"]`); if (input) { input.value = String(fieldValue[subField] || ''); } } }); } /** * Populate user fields (similar to taxonomy) */ populateUserField(fieldWrapper, fieldName, fieldValue) { // Similar logic to taxonomy fields this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue); } /** * Populate repeater fields */ populateRepeaterField(fieldWrapper, fieldName, fieldValue) { if (!fieldValue || !Array.isArray(fieldValue)) { return; } const container = fieldWrapper.querySelector('.repeater-items'); const template = fieldWrapper.querySelector('template'); if (!container || !template) { console.warn(`Repeater field ${fieldName}: missing container or template`); return; } // Clear existing rows window.removeChildren(container); // Create rows for each data item fieldValue.forEach((rowData, index) => { if (!rowData || typeof rowData !== 'object') { return; } const row = window.getTemplate(template.className); if (!row) { console.warn(`Repeater field ${fieldName}: template not found`); return; } // Set row ID and update row number row.id = `${fieldWrapper.closest('form').id}-${fieldName}-row-${index}`; row.dataset.index = index; const rowNumber = row.querySelector('.row-number'); if (rowNumber) { rowNumber.textContent = `#${index + 1}`; } // Update field names and populate values row.querySelectorAll('input, select, textarea').forEach(field => { const originalName = field.name; const newName = `${fieldName}:${index}:${originalName}`; const newId = `${fieldName}-${index}-${originalName}-${field.value}`; // Update field identifiers field.name = newName; field.id = newId; // Update label const label = field.nextElementSibling; if (label && label.tagName === 'LABEL') { label.htmlFor = newId; } // Populate field value if (rowData[originalName] !== undefined) { this.populateRepeaterFieldValue(field, originalName, rowData[originalName]); } }); container.appendChild(row); }); } /** * Populate individual repeater field value */ populateRepeaterFieldValue(field, fieldName, fieldValue) { switch (field.type) { case 'checkbox': field.checked = Boolean(fieldValue); break; case 'radio': field.checked = field.value === String(fieldValue); break; case 'select-one': case 'select-multiple': field.value = String(fieldValue || ''); break; default: field.value = String(fieldValue || ''); } } } // Make available globally window.jvbPopulate = PopulateForm;