/**********************************************
|
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 'selector':
|
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(',');
|
|
if (window.jvbSelector) {
|
requestAnimationFrame(() => {
|
window.jvbSelector.updateFieldFromInput(hiddenInput);
|
});
|
}
|
|
}
|
}
|
|
/**
|
* 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"]`);
|
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"]');
|
|
template.dataset.id = itemId;
|
window.prefixInput(input, `select-item-${itemId}`, true);
|
|
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) {
|
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"]');
|
if (input) {
|
window.prefixInput(input, `select-item-${imageId}`, true);
|
}
|
|
// 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 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}`;
|
window.prefixInput(input, newName, true);
|
});
|
|
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) {
|
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, index) => {
|
const originalName = field.name;
|
const newName = `${fieldName}:${index}:${originalName}`;
|
const prefix = `${fieldName}-${index}-`;
|
|
// Update field identifiers
|
field.name = newName;
|
window.prefixInput(field, prefix);
|
|
// 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;
|