|
class FormFieldsOld {
|
constructor() {
|
this.forms = new Map(); // Store form configurations
|
this.formIndex = 0; // Auto-generate IDs
|
this.initialized = false;
|
this.hasRepeaters = false;
|
this.hasNumberFields = false;
|
this.stepMultiplier = 1;
|
|
console.log(jvbSettings.icons);
|
this.timeouts = new Map();
|
this.activeRepeaters = new Map();
|
this.repeaterTimeouts = new Map(); // Form-level repeater timeouts
|
this.repeaterDelays = {
|
change: 6000, // Delay after field change (longer than current 4250ms)
|
typing: 3000, // Delay for text inputs while typing
|
blur: 1500, // Delay after field loses focus
|
add: 500, // Delay after adding row
|
remove: 800, // Delay after removing row
|
reorder: 1000 // Delay after reordering rows
|
};
|
|
this.ignore = [
|
'image_temp',
|
'post_thumbnail_temp'
|
];
|
|
// Bind methods for event delegation
|
this.submitHandler = this.handleSubmit.bind(this);
|
this.changeHandler = this.handleChange.bind(this);
|
this.clickHandler = this.handleClick.bind(this);
|
this.focusHandler = this.handleFocus.bind(this);
|
this.keyHandler = this.handleKeys.bind(this);
|
this.blurHandler = this.handleBlur.bind(this);
|
|
this.initListeners();
|
}
|
|
/**
|
* Add a form to be managed by this instance
|
* @param {HTMLFormElement} formElement
|
* @param {Object} options
|
* @returns {Object} Form configuration
|
*/
|
addForm(formElement, options = {}) {
|
console.log('addingForm:', formElement);
|
// Ensure form has an ID
|
if (!formElement.id) {
|
formElement.id = `form-${this.formIndex++}`;
|
}
|
|
const formConfig = {
|
element: formElement,
|
id: formElement.id,
|
data: this.collectFormData(formElement),
|
|
// Default options with overrides
|
options: {
|
onSave: false,
|
onChange: null, // Optional change callback
|
onSubmit: null, // Optional submit callback
|
itemID: null,
|
content: null,
|
saveDelay: 2000,
|
autoSave: true,
|
api: formElement.dataset.save || null,
|
headers: { 'action_nonce': jvbSettings?.dash },
|
...options
|
},
|
|
// Form-specific state
|
state: {
|
saveTimeout: null,
|
lastData: null,
|
isDirty: false
|
},
|
dependencies: new Map(),
|
};
|
|
// Store the configuration
|
this.forms.set(formElement.id, formConfig);
|
|
// Initialize form-specific features
|
this.initFormFeatures(formConfig);
|
return formConfig;
|
}
|
|
/**
|
* Initialize global event listeners (single delegation point)
|
*/
|
initListeners() {
|
if (this.initialized) return;
|
|
// Use event delegation for all forms
|
document.addEventListener('submit', this.submitHandler);
|
document.addEventListener('change', this.changeHandler);
|
|
|
document.addEventListener('click', this.clickHandler);
|
|
document.addEventListener('keydown', this.keyHandler);
|
|
document.addEventListener('focusin', this.focusHandler);
|
document.addEventListener('focousout', this.blurHandler);
|
|
this.initialized = true;
|
}
|
|
handleKeys(e) {
|
if (!this.hasNumberFields) {
|
return;
|
}
|
if (e.ctrlKey && e.shiftKey) {
|
this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000);
|
} else if (e.shiftKey) {
|
this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 10, 1000);
|
} else if (e.key === 'Escape') {
|
this.stepMultiplier = 1;
|
}
|
|
}
|
|
/**
|
* Global submit handler - routes to appropriate form
|
*/
|
handleSubmit(event) {
|
const form = event.target.closest('form');
|
if (!form || !this.forms.has(form.id)) return;
|
|
event.preventDefault();
|
const formConfig = this.forms.get(form.id);
|
|
// Call form-specific submit callback if provided
|
if (formConfig.options.onSubmit) {
|
console.log('sending data back to onSubmit...');
|
formConfig.options.onSubmit(event, this.collectFormData(formConfig.element));
|
} else {
|
console.log('processing Form Changes on submit...');
|
// Default submit behavior
|
this.processFormChanges(formConfig);
|
}
|
}
|
|
/**
|
* Global change handler - routes to appropriate form
|
*/
|
handleChange(event) {
|
//Image fields handled separately
|
if (window.targetCheck(event, '.image.field')) {
|
return;
|
}
|
|
if (event.target.closest('.repeater-row')) {
|
this.handleRepeaterChange(event);
|
return;
|
}
|
// if (event.target.closest('.repeater-row')) {
|
// const repeaterRow = event.target.closest('.repeater-row');
|
// const row = repeaterRow.closest('.field.repeater');
|
// const form = event.target.closest('form');
|
// const formConfig = this.forms.get(form.id);
|
// const rowId = repeaterRow.id || this.generateRowId(repeaterRow);
|
//
|
// // Clear existing timeout for this row
|
// if (this.timeouts.has(rowId)) {
|
// clearTimeout(this.timeouts.get(rowId));
|
// }
|
//
|
// // Set longer timeout as fallback
|
// this.timeouts.set(rowId, setTimeout(() => {
|
// this.saveRepeaterChanges(form.id, row, 'timeout');
|
// this.timeouts.delete(rowId);
|
// }, 4250));
|
//
|
// // Check conditionals immediately
|
// let changed = event.target.name;
|
// this.checkConditionals(changed, formConfig);
|
//
|
// return;
|
// }
|
// if (!event.isTrusted) {
|
// return;
|
// }
|
const form = event.target.closest('form');
|
if (!form || !this.forms.has(form.id)) return;
|
|
const formConfig = this.forms.get(form.id);
|
|
let changed = event.target.name;
|
this.checkConditionals(changed, formConfig);
|
|
// Handle specific field types
|
this.handleSpecialFields(event, formConfig);
|
|
if ('noautosave' in form.dataset) {
|
return;
|
}
|
|
// Auto-save if enabled
|
if (formConfig.options.autoSave) {
|
this.scheduleAutoSave(formConfig);
|
}
|
}
|
|
handleRepeaterChange(event) {
|
const repeaterRow = event.target.closest('.repeater-row');
|
const repeater = repeaterRow.closest('.field.repeater');
|
const form = event.target.closest('form');
|
const formConfig = this.forms.get(form.id);
|
|
if (!formConfig) return;
|
|
const fieldName = repeater.dataset.field;
|
const formId = form.id;
|
|
// Check conditionals immediately
|
const changed = event.target.name;
|
this.checkConditionals(changed, formConfig);
|
|
// Determine appropriate delay based on field type and interaction
|
let delay = this.getRepeaterDelay(event.target, event.type);
|
|
// Clear existing timeout for this repeater field in this form
|
const timeoutKey = `${formId}-${fieldName}`;
|
if (this.repeaterTimeouts.has(timeoutKey)) {
|
clearTimeout(this.repeaterTimeouts.get(timeoutKey));
|
}
|
|
// Set new timeout with appropriate delay
|
this.repeaterTimeouts.set(timeoutKey, setTimeout(() => {
|
this.saveRepeaterChanges(formId, repeater, `${event.type}-timeout`);
|
this.repeaterTimeouts.delete(timeoutKey);
|
}, delay));
|
|
console.log(`Scheduled repeater save for ${fieldName} in ${delay}ms (reason: ${event.type})`);
|
}
|
|
/**
|
* Force immediate save of all repeater changes for a form
|
*/
|
forceRepeaterSave(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return;
|
|
const form = formConfig.element;
|
const repeaters = form.querySelectorAll('.field.repeater');
|
|
// Clear all pending timeouts
|
this.clearFormRepeaterTimeouts(formId);
|
|
// Process each repeater immediately
|
repeaters.forEach(repeater => {
|
this.processRepeaterChanges(formConfig, repeater);
|
});
|
}
|
|
/**
|
* Determine appropriate delay for repeater changes
|
*/
|
getRepeaterDelay(field, eventType) {
|
// Text inputs get longer delays to allow for typing
|
if ((field.type === 'text' || field.type === 'textarea') && eventType === 'input') {
|
return this.repeaterDelays.typing;
|
}
|
|
// Other input types get standard change delay
|
if (eventType === 'change') {
|
return this.repeaterDelays.change;
|
}
|
|
// Blur events (when user leaves field) get shorter delay
|
if (eventType === 'blur') {
|
return this.repeaterDelays.blur;
|
}
|
|
// Default to change delay
|
return this.repeaterDelays.change;
|
}
|
|
/**
|
* Global click handler - routes to appropriate form
|
*/
|
handleClick(event) {
|
const form = event.target.closest('form');
|
if (!form || !this.forms.has(form.id)) return;
|
|
const formConfig = this.forms.get(form.id);
|
|
// Handle repeater actions
|
if (event.target.matches('.add-repeater-row')) {
|
const repeater = event.target.closest('.repeater');
|
this.addRepeaterRow(repeater, formConfig);
|
|
// Schedule save after add
|
this.scheduleRepeaterSave(form.id, repeater, 'add', this.repeaterDelays.add);
|
|
} else if (event.target.matches('.remove-row')) {
|
const repeater = event.target.closest('.repeater');
|
this.removeRepeaterRow(event.target, formConfig);
|
|
// Schedule save after remove
|
this.scheduleRepeaterSave(form.id, repeater, 'remove', this.repeaterDelays.remove);
|
|
} else if (event.target.matches('.remove-image')) {
|
this.handleImageRemove(event.target.closest('.field'));
|
} else if (event.target.matches('.replace-image')) {
|
let fileInput = event.closest('.image').querySelector('input[type="file"]');
|
fileInput.click();
|
} else if (window.targetCheck(event, 'div.quantity')) {
|
let quantity = window.targetCheck(event, 'div.quantity');
|
this.handleNumberClick(event, quantity);
|
}
|
// Add other click handlers as needed
|
}
|
|
/**
|
* Schedule repeater save with specific timing
|
*/
|
scheduleRepeaterSave(formId, repeater, reason, delay) {
|
const fieldName = repeater.dataset.field;
|
const timeoutKey = `${formId}-${fieldName}`;
|
|
// Clear existing timeout
|
if (this.repeaterTimeouts.has(timeoutKey)) {
|
clearTimeout(this.repeaterTimeouts.get(timeoutKey));
|
}
|
|
// Set new timeout
|
this.repeaterTimeouts.set(timeoutKey, setTimeout(() => {
|
this.saveRepeaterChanges(formId, repeater, reason);
|
this.repeaterTimeouts.delete(timeoutKey);
|
}, delay));
|
|
console.log(`Scheduled repeater save for ${fieldName} in ${delay}ms (reason: ${reason})`);
|
}
|
|
/**
|
* Handle focus events - track when we enter repeater fields
|
*/
|
handleFocus(event) {
|
const form = event.target.closest('form');
|
if (!form || !this.forms.has(form.id)) return;
|
|
const repeaterRow = event.target.closest('.repeater-row');
|
if (!repeaterRow) return;
|
|
const formConfig = this.forms.get(form.id);
|
const rowId = repeaterRow.id || this.generateRowId(repeaterRow);
|
|
// Store the currently active repeater field
|
this.activeRepeaters.set(form.id, {
|
rowId: rowId,
|
element: event.target,
|
formConfig: formConfig
|
});
|
}
|
|
handleBlur(event) {
|
const form = event.target.closest('form');
|
if (!form || !this.forms.has(form.id)) return;
|
|
// Handle repeater field blur with shorter delay
|
if (event.target.closest('.repeater-row')) {
|
const repeater = event.target.closest('.field.repeater');
|
this.scheduleRepeaterSave(form.id, repeater, 'blur', this.repeaterDelays.blur);
|
}
|
}
|
|
|
/**
|
* Schedule auto-save with debouncing per form
|
*/
|
scheduleAutoSave(formConfig) {
|
// Don't autosave if uploads are pending
|
if (formConfig.state.uploadPending) {
|
console.log('Skipping autosave - uploads pending');
|
return;
|
}
|
|
// Also check global upload status
|
if (this.hasActiveUploads(formConfig)) {
|
console.log('Skipping autosave - active uploads detected');
|
return;
|
}
|
|
// Clear existing timeout for this specific form
|
if (formConfig.state.saveTimeout) {
|
clearTimeout(formConfig.state.saveTimeout);
|
}
|
|
// Set new timeout for this form
|
formConfig.state.saveTimeout = setTimeout(() => {
|
// Double-check upload status before saving
|
if (!formConfig.state.uploadPending && !this.hasActiveUploads(formConfig)) {
|
this.processFormChanges(formConfig);
|
}
|
}, formConfig.options.saveDelay);
|
}
|
|
hasActiveUploads(formConfig) {
|
if (!formConfig.uploadFields || !window.jvbUploadManager) {
|
return false;
|
}
|
|
// Check each upload field for active uploads
|
for (const fieldId of formConfig.uploadFields) {
|
const status = window.jvbUploadManager.getFieldStatus(fieldId);
|
if (status && (status.uploading > 0 || status.ready > 0)) {
|
return true;
|
}
|
}
|
|
return false;
|
}
|
|
/**
|
* Process changes for a specific form
|
*/
|
processFormChanges(formConfig, processSave = true) {
|
console.log('Processing changes...');
|
// Skip if uploads are pending
|
if (formConfig.state.uploadPending || this.hasActiveUploads(formConfig)) {
|
console.log('Skipping form changes processing - uploads pending');
|
|
// Schedule a retry
|
setTimeout(() => {
|
this.processFormChanges(formConfig, processSave);
|
}, 2000);
|
|
return;
|
}
|
|
const newData = this.collectFormData(formConfig.element);
|
const changes = this.getDataChanges(newData, formConfig.data);
|
|
console.log(newData);
|
console.log(changes);
|
|
if (Object.keys(changes).length > 0) {
|
// Update stored data
|
formConfig.data = newData;
|
formConfig.state.isDirty = true;
|
|
// Call the form's save callback
|
if (processSave) {
|
this.handleSave(changes, formConfig);
|
}
|
}
|
}
|
|
|
handleSave(changes, formConfig) {
|
console.log(changes);
|
if (changes.length === 0 || window.isEmptyObject(changes)) {
|
return;
|
}
|
if (typeof formConfig.options.onSave === "function") {
|
formConfig.options.onSave(changes, formConfig);
|
} else if (formConfig.element.dataset.save) {
|
let endpoint = formConfig.element.dataset.save;
|
let title = formConfig.element.dataset.title??endpoint;
|
let operation = {
|
endpoint: endpoint,
|
headers: {
|
'action_nonce': jvbSettings.dash
|
},
|
title: `Adding ${title} to Queue`,
|
popup: `Queueing ${title}...`,
|
data: this.collectFormData(formConfig.element)
|
}
|
window.jvbQueue.addToQueue(operation);
|
}
|
}
|
|
/**
|
* Initialize form-specific features
|
*/
|
initFormFeatures(formConfig) {
|
const form = formConfig.element;
|
|
// Initialize conditional fields
|
this.initConditionalFields(formConfig);
|
|
// Initialize repeater fields
|
this.initRepeaterFields(form);
|
|
// Initialize special field types
|
if (form.querySelector('[data-editor="true"]')) {
|
this.initQuillEditor(formConfig, formConfig.options.itemID);
|
}
|
|
if (form.querySelector('.gallery')) {
|
this.initGalleryFields(formConfig);
|
}
|
|
if (form.querySelector('.image')) {
|
this.initImageFields(formConfig);
|
}
|
|
if (form.querySelector('[data-limit]')) {
|
this.initCharacterLimits(formConfig);
|
}
|
|
if (form.querySelector('input[type="number"]')) {
|
this.initNumberFields(formConfig);
|
}
|
}
|
|
/**
|
* Collect form data into a structured object
|
*/
|
collectFormData(form) {
|
const formData = new FormData(form);
|
let data = {};
|
let repeaterData = {};
|
|
|
for (let [key, value] of formData.entries()) {
|
if (this.ignore.includes(key) || key.endsWith('_temp')) {
|
continue;
|
}
|
|
let post = null;
|
let original = null;
|
|
//If we're on a table view, we need to organize this by post ID
|
if (key.includes('|')) {
|
[post, key] = key.split('|');
|
//Temporarily store data to merge later
|
original = data;
|
data = original[post]??{};
|
}
|
if (key.includes(':')) {
|
// Handle repeater fields (field:index:name)
|
let [fieldName, index, rawSubField] = key.split(':');
|
|
// Handle array fields (remove [] brackets)
|
const isArrayField = rawSubField.endsWith('[]');
|
const subField = rawSubField.replace('[]', '');
|
|
// Initialize repeater structure
|
if (!repeaterData[fieldName]) {
|
repeaterData[fieldName] = {};
|
}
|
if (!repeaterData[fieldName][index]) {
|
repeaterData[fieldName][index] = {};
|
}
|
|
// Handle different field types for repeater
|
let fieldValue = value;
|
|
// Only check radio buttons since FormData handles checkboxes correctly
|
const fieldElement = form.querySelector(`[name="${key}"]`);
|
if (fieldElement && fieldElement.type === 'radio' && !fieldElement.checked) {
|
continue; // Skip unchecked radios
|
}
|
|
// Handle array fields (like checkbox groups)
|
if (isArrayField || 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(fieldValue);
|
} else {
|
// Single value field
|
repeaterData[fieldName][index][subField] = fieldValue;
|
}
|
|
} else {
|
// Handle array values (multiple checkboxes/selects)
|
if (data[key]) {
|
if (!Array.isArray(data[key])) {
|
data[key] = [data[key]];
|
}
|
data[key].push(value);
|
} else {
|
data[key] = value;
|
}
|
}
|
if (post) {
|
//Merge back with original
|
original[post] = data;
|
data = original;
|
}
|
}
|
|
// Merge repeater data into main data structure
|
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;
|
}
|
|
/**
|
* Get differences between old and new data
|
*/
|
getDataChanges(newData, oldData, deep = false) {
|
// Use your existing getDifferences utility or implement comparison logic
|
return window.getDifferences?.map(oldData, newData) || {};
|
}
|
|
/**
|
* Handle special field types (repeaters, conditionals, etc.)
|
*/
|
handleSpecialFields(event, formConfig) {
|
// Handle repeater field changes
|
if (event.target.closest('.repeater-row')) {
|
this.updateRepeaterOrder(event.target.closest('.repeater'));
|
}
|
|
// Handle conditional field triggers
|
const triggerName = event.target.name;
|
this.checkConditionals(triggerName, formConfig);
|
}
|
|
/**
|
* Initialize conditional fields for a specific form
|
*/
|
initConditionalFields(formConfig) {
|
const form = formConfig.element;
|
const conditionalFields = form.querySelectorAll('[data-depends-on]');
|
|
conditionalFields.forEach(field => {
|
const changed = field.dataset.dependsOn;
|
const triggerValue = this.getFieldValue(form, changed);
|
|
const requiredValue = field.dataset.dependsValue;
|
const operator = field.dataset.dependsOperator || '==';
|
const shouldShow = this.evaluateCondition(triggerValue, requiredValue, operator);
|
|
this.toggleFieldVisibility(field, shouldShow);
|
});
|
}
|
|
checkConditionals(changed, formConfig) {
|
if (formConfig.dependencies.has(changed) && formConfig.dependencies.get(changed)=== false) {
|
return;
|
}
|
let dependencies = formConfig.element.querySelectorAll(`[data-depends-on="${changed}"]`);
|
formConfig.dependencies.set(changed, (dependencies.length > 0) ? dependencies : false);
|
if (dependencies.length === 0) {
|
return;
|
}
|
this.updateConditionalFields(changed, dependencies, formConfig);
|
}
|
/**
|
* Update conditional fields based on trigger values
|
*/
|
updateConditionalFields(changed, dependencies, formConfig) {
|
|
const triggerValue = this.getFieldValue(formConfig.element, changed);
|
|
dependencies.forEach(field => {
|
this.checkDependencies(field, triggerValue);
|
});
|
}
|
|
checkDependencies(field, value) {
|
const requiredValue = field.dataset.dependsValue;
|
const operator = field.dataset.dependsOperator || '==';
|
const shouldShow = this.evaluateCondition(value, requiredValue, operator);
|
this.toggleFieldVisibility(field, shouldShow);
|
}
|
|
/**
|
* Get field value considering different input types
|
*/
|
getFieldValue(form, fieldName) {
|
const field = form.querySelector(`[name="${fieldName}"]`);
|
if (!field) return null;
|
|
if (field.type === 'radio') {
|
const checked = form.querySelector(`[name="${fieldName}"]:checked`);
|
return checked ? checked.value : null;
|
} else if (field.type === 'checkbox') {
|
return field.checked ? (field.value || '1') : '';
|
}
|
|
return field.value;
|
}
|
|
/**
|
* Evaluate conditional logic
|
*/
|
evaluateCondition(fieldValue, requiredValue, operator) {
|
// Handle null/undefined values
|
if (fieldValue === null || fieldValue === undefined) {
|
fieldValue = '';
|
}
|
if (requiredValue === null || requiredValue === undefined) {
|
requiredValue = '';
|
}
|
|
// Convert both to strings for consistent comparison (since form values are always strings)
|
const fieldStr = String(fieldValue);
|
const requiredStr = String(requiredValue);
|
|
switch (operator) {
|
case '==':
|
return fieldStr == requiredStr; // Loose equality
|
case '!=':
|
return fieldStr != requiredStr; // Loose inequality
|
case '===':
|
return fieldStr === requiredStr; // Strict equality (if needed)
|
case '!==':
|
return fieldStr !== requiredStr; // Strict inequality (if needed)
|
case '>':
|
return Number(fieldValue) > Number(requiredValue);
|
case '>=':
|
return Number(fieldValue) >= Number(requiredValue);
|
case '<':
|
return Number(fieldValue) < Number(requiredValue);
|
case '<=':
|
return Number(fieldValue) <= Number(requiredValue);
|
case 'contains':
|
return fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
|
case 'not_contains':
|
return !fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
|
case 'starts_with':
|
return fieldStr.toLowerCase().startsWith(requiredStr.toLowerCase());
|
case 'ends_with':
|
return fieldStr.toLowerCase().endsWith(requiredStr.toLowerCase());
|
case 'empty':
|
return fieldStr.trim() === '';
|
case 'not_empty':
|
return fieldStr.trim() !== '';
|
case 'in':
|
// For array/comma-separated values: requiredValue = "option1,option2,option3"
|
const options = requiredStr.split(',').map(opt => opt.trim());
|
return options.includes(fieldStr);
|
case 'not_in':
|
const notOptions = requiredStr.split(',').map(opt => opt.trim());
|
return !notOptions.includes(fieldStr);
|
default:
|
return false;
|
}
|
}
|
|
|
/**
|
* Debug helper to see what's happening
|
*/
|
debugCondition(fieldValue, requiredValue, operator) {
|
console.group('🔍 Conditional Field Debug');
|
console.log('Field Value:', fieldValue, typeof fieldValue);
|
console.log('Required Value:', requiredValue, typeof requiredValue);
|
console.log('Operator:', operator);
|
console.log('String Field:', String(fieldValue));
|
console.log('String Required:', String(requiredValue));
|
console.log('=== comparison:', String(fieldValue) === String(requiredValue));
|
console.log('== comparison:', String(fieldValue) == String(requiredValue));
|
console.log('!== comparison:', String(fieldValue) !== String(requiredValue));
|
console.log('!= comparison:', String(fieldValue) != String(requiredValue));
|
|
const result = this.evaluateCondition(fieldValue, requiredValue, operator);
|
console.log('Final Result:', result);
|
console.groupEnd();
|
|
return result;
|
}
|
|
/**
|
* 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;
|
});
|
}
|
|
// Stub methods for specific field types
|
initRepeaterFields(form) {
|
form.querySelectorAll('.repeater').forEach(repeater => {
|
this.hasRepeaters = true;
|
const addButton = repeater.querySelector('.add-repeater-row');
|
let temp = repeater.querySelector('template').className;
|
let template = window.getTemplate(temp);
|
const container = repeater.querySelector('.repeater-items');
|
|
if (!addButton || !template || !container) {
|
console.warn('Missing required repeater elements:',
|
{addButton, template, container});
|
return;
|
}
|
|
// Initialize Sortable
|
new Sortable(container, {
|
handle: '.repeater-row-header',
|
animation: 150,
|
onEnd: () => {
|
this.updateRepeaterOrder(repeater);
|
}
|
});
|
|
});
|
}
|
|
initQuillEditor(formConfig, itemID) {
|
let form = formConfig.element;
|
const textareas = form.querySelectorAll('textarea[data-editor=true]');
|
|
textareas.forEach(textarea => {
|
let container, editor, toolbar;
|
//create it if it doesn't exist
|
if(!textarea.parentNode.querySelector('.editor-container')){
|
container = document.createElement('div');
|
container.className = 'editor-container';
|
|
editor = document.createElement('div');
|
editor.className = 'editor';
|
toolbar = document.createElement('div');
|
toolbar.className = 'toolbar';
|
const image = textarea.dataset.allowimage === true ? `<button type="button" class="ql-jvb_image">\n ${dashboardSettings.icons.image}\n </button>` : '';
|
toolbar.id = `toolbar-${textarea.id}`;
|
toolbar.innerHTML = `
|
<span class="ql-formats">
|
<button type="button" class="ql-p">
|
${jvbSettings.icons.paragraph}
|
</button>
|
<button type="button" class="ql-h1">
|
${jvbSettings.icons.h1}
|
</button>
|
<button type="button" class="ql-h2">
|
${jvbSettings.icons.h2}
|
</button>
|
<button type="button" class="ql-h3">
|
${jvbSettings.icons.h3}
|
</button>
|
</span>
|
<span class="ql-formats">
|
<button type="button" class="ql-jvb_bold">
|
${jvbSettings.icons['bold']}
|
</button>
|
<button type="button" class="ql-jvb_italic">
|
${jvbSettings.icons['italic']}
|
</button>
|
<button type="button" class="ql-jvb_underline">
|
${jvbSettings.icons['underline']}
|
</button>
|
<button type="button" class="ql-jvb_strike">
|
${jvbSettings.icons['strike']}
|
</button>
|
</span>
|
<span class="ql-formats">
|
<button type="button" class="ql-jvb_list" value="bullet">
|
${jvbSettings.icons['list-bullets']}
|
</button>
|
<button type="button" class="ql-jvb_list" value="ordered">
|
${jvbSettings.icons['list-numbers']}
|
</button>
|
</span>
|
<span class="ql-formats">
|
<button type="button" class="ql-jvb_align" value="left">
|
${jvbSettings.icons['align-left']}
|
</button>
|
<button type="button" class="ql-jvb_align" value="center">
|
${jvbSettings.icons['align-center']}
|
</button>
|
<button type="button" class="ql-jvb_align" value="right">
|
${jvbSettings.icons['align-right']}
|
</button>
|
</span>
|
<span class="ql-formats">
|
<button type="button" class="ql-jvb_link">
|
${jvbSettings.icons.link}
|
</button>
|
${image}
|
</span>
|
`;
|
|
container.appendChild(toolbar);
|
container.appendChild(editor);
|
textarea.parentNode.insertBefore(container, textarea);
|
textarea.style.display = 'none';
|
editor.innerHTML = textarea.value;
|
}else{
|
container = textarea.parentNode.querySelector('.editor-container');
|
editor = container.querySelector('.editor');
|
toolbar = container.querySelector('.toolbar');
|
}
|
|
|
|
const quill = new Quill(editor, {
|
theme: 'snow',
|
modules: {
|
toolbar: {
|
container: toolbar,
|
handlers: {
|
p: function() { this.quill.format('header', false); },
|
h1: function() { this.quill.format('header', 1); },
|
h2: function() { this.quill.format('header', 2); },
|
h3: function() { this.quill.format('header', 3); },
|
jvb_bold: function() {this.quill.format('bold', true)},
|
jvb_italic: function() {this.quill.format('italic', true)},
|
jvb_strike: function() {this.quill.format('strike', true)},
|
jvb_underline: function() {this.quill.format('underline', true)},
|
'jvb_align': function(value) {
|
this.quill.format('align', value === this.quill.getFormat().list ? false : value);
|
},
|
'jvb_list': function(value) {
|
this.quill.format('list', value === this.quill.getFormat().list ? false : value);
|
},
|
'jvb_link': function(value) {
|
if (value) {
|
const range = this.quill.getSelection();
|
if (range == null || range.length === 0) return;
|
// Get the existing link if any
|
const preview = this.quill.getText(range.index, range.length);
|
const existingLink = this.quill.getFormat(range).link;
|
|
// Create modal for link input
|
const modal = document.createElement('dialog');
|
modal.className = 'quill-link-modal';
|
modal.innerHTML = `
|
<div class="quill-link-modal-content ">
|
<label for="link">Enter URL</label>
|
<input type="url" id="link" placeholder="Enter URL" value="${existingLink || ''}" />
|
<div class="buttons">
|
<button type="button" class="save">Save</button>
|
${existingLink ? '<button type="button" class="remove">Remove</button>' : ''}
|
<button type="button" class="cancel">Cancel</button>
|
</div>
|
</div>
|
`;
|
|
document.body.appendChild(modal);
|
modal.showModal();
|
const input = modal.querySelector('input');
|
input.focus();
|
|
// Handle save
|
modal.querySelector('.save').addEventListener('click', () => {
|
const url = input.value;
|
if (url) {
|
this.quill.format('link', url);
|
}
|
modal.remove();
|
});
|
|
// Handle remove if link exists
|
const removeBtn = modal.querySelector('.remove');
|
if (removeBtn) {
|
removeBtn.addEventListener('click', () => {
|
this.quill.format('link', false);
|
modal.remove();
|
});
|
}
|
|
// Handle cancel
|
modal.querySelector('.cancel').addEventListener('click', () => {
|
modal.remove();
|
});
|
|
// Handle Enter key
|
input.addEventListener('keyup', (e) => {
|
if (e.key === 'Enter') {
|
const url = input.value;
|
if (url) {
|
this.quill.format('link', url);
|
}
|
modal.remove();
|
}
|
});
|
}
|
},
|
'jvb_image': function() {
|
const input = document.createElement('input');
|
input.setAttribute('type', 'file');
|
input.setAttribute('accept', 'image/jpeg,image/png,image/gif,image/webp');
|
input.style.display = 'none';
|
document.body.appendChild(input);
|
|
input.onchange = async (e) => {
|
const file = e.target.files?.[0];
|
if (!file) return;
|
|
// Validate file
|
const maxSize = 5242880; // 5MB
|
if (file.size > maxSize) {
|
this.quill.insertText(range.index, 'File too large. Maximum size is 5MB', {
|
'color': '#f00',
|
'italic': true
|
}, true);
|
input.remove();
|
return;
|
}
|
|
const range = this.quill.getSelection(true);
|
const formData = new FormData();
|
formData.append('image', file);
|
|
if (objectID) {
|
formData.append('post_id', objectID);
|
}
|
|
// Show loading state
|
if (window.jvbLoading) {
|
window.jvbLoading.showLoading('Uploading image...', 'Processing Upload');
|
}
|
|
try {
|
const response = await fetch(
|
`${jvbSettings.api}uploads/`,
|
{
|
method: 'POST',
|
headers: {
|
'X-WP-Nonce': jvbSettings.nonce
|
},
|
body: formData
|
}
|
);
|
|
if (!response.ok) {
|
throw new Error('Upload failed');
|
}
|
|
const result = await response.json();
|
|
// Insert the image at cursor position
|
this.quill.insertEmbed(range.index, 'image', result.url);
|
|
} catch (error) {
|
console.error('Upload error:', error);
|
this.quill.insertText(range.index, 'Failed to upload image. Please try again.', {
|
'color': '#f00',
|
'italic': true
|
}, true);
|
} finally {
|
if (window.jvbLoading) {
|
window.jvbLoading.hide();
|
}
|
input.remove();
|
}
|
};
|
|
input.click();
|
}
|
}
|
},
|
history: {
|
delay: 2000,
|
maxStack: 500
|
},
|
clipboard: {
|
matchVisual: false
|
}
|
}
|
});
|
|
quill.on('selection-change', function(range) {
|
const alignmentTools = toolbar.querySelector('.ql-align');
|
if (alignmentTools) {
|
if (range && range.length === 0) {
|
// Get the focused element
|
const [leaf] = this.quill.getLeaf(range.index);
|
if (leaf && leaf.domNode && leaf.domNode.tagName === 'IMG') {
|
alignmentTools.style.display = 'inline-block';
|
return;
|
}
|
}
|
alignmentTools.style.display = 'none';
|
}
|
});
|
// Update hidden textarea and trigger form change
|
quill.on('text-change', () => {
|
textarea.value = quill.root.innerHTML;
|
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
});
|
});
|
}
|
initGalleryFields(formConfig) {
|
formConfig.element.querySelectorAll('.gallery').forEach(field => {
|
const fieldName = field.querySelector('input[type="hidden"]').name;
|
const previewGrid = field.querySelector('.gallery-preview');
|
|
// Pre-populate existing images
|
if (field.dataset.images) {
|
const urls = field.dataset.images.split(',');
|
urls.forEach(url => {
|
this.addToGalleryPreview(url, previewGrid);
|
});
|
}
|
|
// Register with centralized upload manager
|
const fieldId = window.jvbUploadManager.registerUploader(field, {
|
content: formConfig.options.content,
|
postId: formConfig.options.itemID,
|
mode: 'gallery',
|
uploadType: 'image_upload',
|
fieldName: fieldName,
|
maxFiles: 20,
|
allowMultiple: true,
|
groupable: true,
|
|
onUploadComplete: (result) => {
|
this.handleGalleryUploadSuccess(result, field);
|
},
|
});
|
|
// Store field reference
|
if (!formConfig.uploadFields) {
|
formConfig.uploadFields = new Set();
|
}
|
formConfig.uploadFields.add(fieldId);
|
});
|
}
|
|
handleGalleryUploadSuccess(result, field) {
|
console.log('Gallery Upload success!', result);
|
|
if (!result.data || !result.data.length) return;
|
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
const previewGrid = field.querySelector('.gallery-preview');
|
const currentIds = hiddenInput.value ? hiddenInput.value.split(',') : [];
|
|
result.data.forEach(file => {
|
currentIds.push(file.attachment_id);
|
this.addToGalleryPreview(file.url, previewGrid);
|
});
|
|
hiddenInput.value = currentIds.join(',');
|
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
this.showNotification(`Added ${result.data.length} image(s) to gallery`);
|
}
|
addToGalleryPreview(url, grid) {
|
let preview = window.getTemplate('galleryPreview');
|
let img = preview.querySelector('img');
|
img.src = url;
|
|
grid.appendChild(preview);
|
return preview;
|
}
|
initImageFields(formConfig) {
|
formConfig.element.querySelectorAll('.image').forEach(field => {
|
const fieldName = field.querySelector('input[type="hidden"]').name;
|
const fileInput = field.querySelector('input[type="file"]');
|
|
if (!fileInput) return;
|
|
// Register with centralized upload manager
|
const fieldId = window.jvbUploadManager.registerUploader(field, {
|
content: formConfig.options.content,
|
postId: formConfig.options.itemID,
|
mode: 'direct',
|
uploadType: 'image_upload',
|
fieldName: fieldName,
|
maxFiles: 1,
|
autoSubmit: true,
|
|
// Custom callbacks for this field type
|
onUploadStart: () => {
|
formConfig.state.uploadPending = true;
|
console.log('Image upload started - pausing autosave');
|
},
|
|
onUploadComplete: (result) => {
|
formConfig.state.uploadPending = false;
|
this.handleImageUploadSuccess(result, field);
|
},
|
|
onUploadError: (error) => {
|
formConfig.state.uploadPending = false;
|
this.handleImageUploadError(error, field);
|
}
|
});
|
|
// Store field reference for cleanup
|
if (!formConfig.uploadFields) {
|
formConfig.uploadFields = new Set();
|
}
|
formConfig.uploadFields.add(fieldId);
|
});
|
}
|
|
|
handleImageUploadSuccess(result, field) {
|
console.log('Image Upload success!', result);
|
|
if (!result.data || !result.data.length) return;
|
|
const imageDisplay = field.querySelector('.image-display');
|
removeChildren(imageDisplay);
|
imageDisplay.classList.add('has-image');
|
|
let ids = [];
|
result.data.forEach(file => {
|
let img = new Image();
|
img.src = file.url;
|
ids.push(file.attachment_id);
|
imageDisplay.appendChild(img);
|
});
|
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
hiddenInput.value = ids.join(',');
|
|
const uploadContainer = field.querySelector('.file-upload-container');
|
uploadContainer.hidden = true;
|
|
// Trigger form change event
|
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
this.showNotification('Image updated successfully');
|
}
|
|
hasUploadsPending(formConfig) {
|
return formConfig.state.uploadPending || false;
|
}
|
forceSaveAfterUploads(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return;
|
|
// Check periodically if uploads are done
|
const checkUploads = () => {
|
if (!formConfig.state.uploadPending && !this.hasActiveUploads(formConfig)) {
|
console.log('All uploads complete, processing form changes');
|
this.processFormChanges(formConfig);
|
return;
|
}
|
|
console.log('Still waiting for uploads to complete...');
|
setTimeout(checkUploads, 500);
|
};
|
|
checkUploads();
|
}
|
getUploadStatus(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return null;
|
|
return {
|
uploadPending: formConfig.state.uploadPending,
|
isDirty: formConfig.state.isDirty,
|
hasTimeout: !!formConfig.state.saveTimeout
|
};
|
}
|
handleImageUploadError(error, field) {
|
console.error('Upload error:', error);
|
|
// Clear upload pending state
|
const form = field.closest('form');
|
if (form && form.id) {
|
const formConfig = this.forms.get(form.id);
|
if (formConfig) {
|
formConfig.state.uploadPending = false;
|
}
|
}
|
|
this.showNotification('Failed to upload image', 'error');
|
|
// Reset field if needed
|
const uploadContainer = field.querySelector('.file-upload-container');
|
uploadContainer.hidden = false;
|
|
// Clear any error states
|
const errorElement = field.querySelector('.file-error');
|
if (errorElement) {
|
errorElement.textContent = '';
|
}
|
}
|
handleImageRemove(field) {
|
const imageDisplay = field.querySelector('.image-display');
|
const img = imageDisplay.querySelector('img');
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
const uploadContainer = field.querySelector('.file-upload-container');
|
|
// Clear the hidden input
|
hiddenInput.value = '';
|
|
// Reset UI
|
img.src = '';
|
imageDisplay.classList.remove('has-image');
|
uploadContainer.hidden = false;
|
|
// Show notification
|
this.showNotification('Image removed');
|
}
|
initCharacterLimits(formConfig) {
|
formConfig.element.querySelectorAll('input[data-limit], textarea[data-limit]').forEach(input => {
|
const counter = input.closest('.field').querySelector('.char-count .current');
|
if (counter) {
|
const updateCount = () => {
|
const limit = parseInt(input.dataset.limit, 10);
|
|
// Update the counter
|
counter.textContent = input.value.length;
|
|
// If length exceeds limit, truncate the input value
|
if (input.value.length > limit) {
|
input.closest('.field').classList.add('reached');
|
input.value = input.value.substring(0, limit);
|
counter.textContent = limit; // Update counter after truncation
|
}else {
|
input.closest('.field').classList.remove('reached');
|
}
|
};
|
|
input.addEventListener('input', updateCount);
|
updateCount(); // Initial count
|
}
|
})
|
}
|
|
initNumberFields(formConfig) {
|
this.hasNumberFields = true;
|
}
|
handleNumberClick(e, container) {
|
let change = 0;
|
if (e.target.closest('.increase')) {
|
change += 1;
|
} else if (e.target.closest('.decrease')) {
|
change -=1;
|
}
|
if (change !== 0) {
|
let [
|
step,
|
input
|
] = [
|
parseInt(container.dataset.step),
|
container.querySelector('input'),
|
];
|
|
let value = (input.value === '') ? 0 : parseInt(input.value);
|
|
input.value = (value + (step * change * this.stepMultiplier));
|
this.handleNumberLimits(container);
|
}
|
}
|
|
handleNumberLimits(container) {
|
let [
|
min,
|
max,
|
input,
|
increase,
|
decrease
|
] = [
|
container.dataset.min,
|
container.dataset.max,
|
container.querySelector('input'),
|
container.querySelector('.increase'),
|
container.querySelector('.decrease')
|
];
|
let value = parseInt(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;
|
}
|
}
|
addRepeaterRow(repeater, formConfig) {
|
let rows = repeater.querySelector('.repeater-items');
|
let index = rows.children.length;
|
|
let row = window.getTemplate(repeater.querySelector('template').className);
|
|
// Set a proper ID for the row
|
row.id = `${formConfig.id}-${repeater.dataset.field}-row-${index}`;
|
|
let base = repeater.dataset.field;
|
row.querySelectorAll('input, select, textarea').forEach((field) => {
|
let label = field.nextElementSibling;
|
if (!label || label.tagName !== 'LABEL') {
|
label = field.closest('.field')?.querySelector(`label`);
|
}
|
let name = `${base}:${index}:${field.name}`;
|
let id = `${base}:${index}:${field.name}-${field.value}`;
|
|
[field.name, field.id, label.htmlFor] = [name, id, id];
|
});
|
|
[row.dataset.index, row.querySelector('.row-number').textContent] = [index, `#${index + 1}`];
|
rows.append(row);
|
|
// Focus the first input in the new row
|
const firstInput = row.querySelector('input, select, textarea');
|
if (firstInput) {
|
firstInput.focus();
|
}
|
}
|
|
/**
|
* Save repeater changes with reason tracking
|
*/
|
saveRepeaterChanges(formId, repeater, reason = 'manual') {
|
console.log(`Saving repeater changes due to: ${reason}`);
|
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return;
|
|
// Clear any other pending timeouts for this form's repeaters
|
this.clearFormRepeaterTimeouts(formId);
|
|
// Use the main form processing pipeline for consistency
|
// but process only repeater changes
|
this.processRepeaterChanges(formConfig, repeater);
|
|
// Clean up active field tracking
|
this.activeRepeaters.delete(formId);
|
}
|
|
processRepeaterChanges(formConfig, repeater) {
|
// Skip if uploads are pending
|
if (formConfig.state.uploadPending || this.hasActiveUploads(formConfig)) {
|
console.log('Skipping repeater save - uploads pending');
|
|
// Schedule a retry
|
setTimeout(() => {
|
this.processRepeaterChanges(formConfig, repeater);
|
}, 2000);
|
return;
|
}
|
|
const fieldName = repeater.dataset.field;
|
|
// Get the complete current form data (including all repeaters)
|
const currentData = this.collectFormData(formConfig.element);
|
|
// Create a change object containing only the repeater field
|
// This ensures we send the complete repeater data
|
const repeaterChanges = {
|
[fieldName]: currentData[fieldName] || []
|
};
|
|
console.log(`Repeater ${fieldName} complete data:`, repeaterChanges);
|
|
// Update stored data for this field only
|
if (!formConfig.data) {
|
formConfig.data = {};
|
}
|
formConfig.data[fieldName] = currentData[fieldName];
|
formConfig.state.isDirty = true;
|
|
// Send the complete repeater data
|
this.handleSave(repeaterChanges, formConfig);
|
}
|
|
/**
|
* Clear all repeater timeouts for a specific form
|
*/
|
clearFormRepeaterTimeouts(formId) {
|
const form = document.getElementById(formId);
|
if (!form) return;
|
|
// Clear timeouts by form and field pattern
|
for (let [timeoutKey, timeout] of this.repeaterTimeouts) {
|
if (timeoutKey.startsWith(formId + '-')) {
|
clearTimeout(timeout);
|
this.repeaterTimeouts.delete(timeoutKey);
|
}
|
}
|
|
// Also clear the old individual row timeouts for backwards compatibility
|
const repeaterRows = form.querySelectorAll('.repeater-row');
|
repeaterRows.forEach(row => {
|
const rowId = row.id || this.generateRowId(row);
|
if (this.timeouts.has(rowId)) {
|
clearTimeout(this.timeouts.get(rowId));
|
this.timeouts.delete(rowId);
|
}
|
});
|
}
|
|
/**
|
* Generate a consistent ID for repeater rows
|
*/
|
generateRowId(repeaterRow) {
|
if (repeaterRow.id) return repeaterRow.id;
|
|
// Generate ID based on form and row position
|
const form = repeaterRow.closest('form');
|
const repeater = repeaterRow.closest('.repeater');
|
const index = Array.from(repeater.querySelectorAll('.repeater-row')).indexOf(repeaterRow);
|
const repeaterId = repeater.dataset.field || 'repeater';
|
|
const generatedId = `${form.id}-${repeaterId}-row-${index}`;
|
repeaterRow.id = generatedId;
|
return generatedId;
|
}
|
removeRepeaterRow(removeButton, formConfig) {
|
let repeater = removeButton.closest('.repeater');
|
removeButton.closest('.repeater-row').remove();
|
this.updateRepeaterOrder(repeater);
|
}
|
updateRepeaterOrder(repeater) {
|
let items = repeater.querySelector('.repeater-items');
|
let base = repeater.dataset.field;
|
const form = repeater.closest('form');
|
const formConfig = this.forms.get(form.id);
|
|
items.querySelectorAll('.repeater-row').forEach((row, index) => {
|
[
|
row.dataset.index,
|
row.querySelector('.row-number').textContent
|
] = [
|
index,
|
`#${index + 1}`
|
];
|
|
row.querySelectorAll('[name]').forEach(field => {
|
let name = field.name.split(':').pop();
|
let newName = `${base}:${index}:${name}`;
|
let newId = `${base}-${index}-${name}`;
|
[
|
field.name,
|
field.id
|
] = [
|
newName,
|
newId
|
];
|
let label = field.closest('.field').querySelector('label');
|
if (label) {
|
label.htmlFor = newId;
|
}
|
});
|
});
|
|
// Schedule save after reorder with appropriate delay
|
this.scheduleRepeaterSave(form.id, repeater, 'reorder', this.repeaterDelays.reorder);
|
}
|
|
|
showNotification(msg, type){
|
window.jvbNotifications.showToast(msg, type);
|
}
|
/**
|
* Public API methods
|
*/
|
|
// Get form configuration
|
getForm(formId) {
|
return this.forms.get(formId);
|
}
|
|
// Manually trigger form processing
|
processForm(formId) {
|
const formConfig = this.forms.get(formId);
|
if (formConfig) {
|
this.processFormChanges(formConfig);
|
}
|
}
|
|
submitFormUploads(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig || !formConfig.uploadFields) return;
|
|
console.log(`Submitting uploads for form: ${formId}`);
|
|
// Submit each upload field
|
formConfig.uploadFields.forEach(fieldId => {
|
window.jvbUploadManager.submitFieldUploads(fieldId);
|
});
|
}
|
|
isFormReadyToSave(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return true;
|
|
return !formConfig.state.uploadPending && !this.hasActiveUploads(formConfig);
|
}
|
|
getFormUploadProgress(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig || !formConfig.uploadFields) return null;
|
|
let totalUploads = 0;
|
let completedUploads = 0;
|
let failedUploads = 0;
|
|
formConfig.uploadFields.forEach(fieldId => {
|
const status = window.jvbUploadManager.getFieldStatus(fieldId);
|
if (status) {
|
totalUploads += status.uploadCount;
|
completedUploads += status.completed;
|
failedUploads += status.failed;
|
}
|
});
|
|
return {
|
total: totalUploads,
|
completed: completedUploads,
|
failed: failedUploads,
|
pending: totalUploads - completedUploads - failedUploads,
|
progress: totalUploads > 0 ? (completedUploads / totalUploads) * 100 : 0
|
};
|
}
|
showFormUploadStatus(formId) {
|
const progress = this.getFormUploadProgress(formId);
|
if (!progress || progress.total === 0) return;
|
|
const message = `Uploads: ${progress.completed}/${progress.total} complete` +
|
(progress.failed > 0 ? `, ${progress.failed} failed` : '') +
|
(progress.pending > 0 ? `, ${progress.pending} pending` : '');
|
|
this.showNotification(message, progress.failed > 0 ? 'warning' : 'info');
|
}
|
getFormStatus(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return null;
|
|
const baseStatus = {
|
formId,
|
isDirty: formConfig.state.isDirty,
|
uploadPending: formConfig.state.uploadPending,
|
hasTimeout: !!formConfig.state.saveTimeout
|
};
|
|
// Add upload status if upload fields exist
|
if (formConfig.uploadFields && window.jvbUploadManager) {
|
baseStatus.uploads = {};
|
formConfig.uploadFields.forEach(fieldId => {
|
baseStatus.uploads[fieldId] = window.jvbUploadManager.getFieldStatus(fieldId);
|
});
|
}
|
|
return baseStatus;
|
}
|
|
// Remove form from management
|
removeForm(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return;
|
|
// Clean up regular timeouts
|
if (formConfig.state.saveTimeout) {
|
clearTimeout(formConfig.state.saveTimeout);
|
}
|
|
// Clean up repeater timeouts
|
this.clearFormRepeaterTimeouts(formId);
|
|
// Clean up upload fields
|
if (formConfig.uploadFields && window.jvbUploadManager) {
|
formConfig.uploadFields.forEach(fieldId => {
|
console.log(`Cleaning up upload field: ${fieldId}`);
|
});
|
}
|
|
this.forms.delete(formId);
|
}
|
|
/***************************************************
|
*
|
* Field rendering from json data
|
*
|
**************************************************/
|
populatePostsTableFields(form, postsData) {
|
if (!form || !postsData) {
|
return;
|
}
|
const formConfig = this.forms.get(form.id);
|
form.querySelectorAll('tr').forEach(row => {
|
let base = row.dataset.id;
|
let fields = JSON.parse(row.dataset.fields);
|
let images = JSON.parse(row.dataset.images);
|
|
this.populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData, options);
|
});
|
}
|
/**
|
* Populate form fields with data values
|
* @param {HTMLFormElement} form - The form element
|
* @param {Object} fieldsData - Field values from API
|
* @param {Object} imagesData - Image metadata (optional)
|
* @param {Object} options - Additional options
|
*/
|
populateFormFields(form, fieldsData, imagesData = {}, options = {}) {
|
if (!form || !fieldsData) {
|
console.warn('FormFields: Missing form or data for population');
|
return;
|
}
|
|
console.log('Populating form fields:', { fieldsData, imagesData });
|
console.log('Form element:', form);
|
console.log('Form fields found:', form.querySelectorAll('.field').length);
|
|
// Get form configuration
|
const formConfig = this.forms.get(form.id);
|
|
// Debug: List all fields in form
|
const allFields = form.querySelectorAll('.field');
|
|
// Try alternative approach if no .field elements found
|
if (allFields.length === 0) {
|
|
// Try finding fields by name attribute instead
|
Object.keys(fieldsData).forEach(fieldName => {
|
const input = form.querySelector(`[name="${fieldName}"]`);
|
if (input) {
|
const fieldWrapper = input.closest('.field') || input.parentElement;
|
console.log(`Found field ${fieldName} via name attribute:`, fieldWrapper);
|
|
if (fieldWrapper) {
|
this.populateFieldValue(fieldWrapper, fieldName, fieldsData[fieldName], imagesData, options);
|
}
|
}
|
});
|
|
return;
|
}
|
|
// Process each field in the form
|
allFields.forEach((fieldWrapper, index) => {
|
console.log(`Processing field ${index}:`, {
|
element: fieldWrapper,
|
tagName: fieldWrapper.tagName,
|
className: fieldWrapper.className,
|
dataset: fieldWrapper.dataset
|
});
|
|
const fieldName = fieldWrapper.dataset.field;
|
console.log(`Field ${index} name:`, fieldName);
|
|
if (!fieldName) {
|
console.warn(`Field ${index} has no data-field attribute`);
|
return;
|
}
|
|
if (!(fieldName in fieldsData)) {
|
console.log(`Field ${fieldName} not in data, skipping`);
|
return;
|
}
|
|
const fieldValue = fieldsData[fieldName];
|
console.log(`About to populate field ${fieldName}:`, { fieldWrapper, fieldValue });
|
|
this.populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData, options);
|
});
|
|
// Process form changes to update any conditional fields
|
if (formConfig) {
|
this.processFormChanges(formConfig, false);
|
}
|
}
|
|
/**
|
* 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
|
*/
|
populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData = {}, options = {}) {
|
if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
|
return;
|
}
|
|
console.log(`Populating field: ${fieldName}`, { fieldValue, fieldWrapper });
|
|
// Determine field type from classes or data attributes
|
const fieldType = this.getFieldType(fieldWrapper);
|
|
switch (fieldType) {
|
case 'image':
|
this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
|
break;
|
|
case 'gallery':
|
this.populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData);
|
break;
|
|
case 'repeater':
|
this.populateRepeaterField(fieldWrapper, fieldName, fieldValue, options);
|
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;
|
}
|
}
|
|
/**
|
* Determine field type from wrapper element
|
* @param {HTMLElement} fieldWrapper - Field wrapper element
|
* @returns {string} Field type
|
*/
|
getFieldType(fieldWrapper) {
|
// Check for specific field classes
|
const typeClasses = [
|
'image', 'gallery', '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;
|
}
|
}
|
|
// 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') {
|
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 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) {
|
console.log(`📝 populateTextareaField called for ${fieldName}:`, {
|
fieldWrapper,
|
fieldName,
|
fieldValue
|
});
|
|
const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
|
fieldWrapper.querySelector('textarea:not([data-editor="true"])');
|
|
console.log(`Found textarea for ${fieldName}:`, textarea);
|
|
if (textarea) {
|
const oldValue = textarea.value;
|
textarea.value = String(fieldValue || '');
|
|
console.log(`Set textarea value for ${fieldName}:`, {
|
oldValue,
|
newValue: textarea.value,
|
actualValue: textarea.value
|
});
|
|
// 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');
|
}
|
|
console.log(`Updated character counter for ${fieldName}:`, {
|
length: textarea.value.length,
|
limit,
|
counterText: counter.textContent
|
});
|
}
|
}
|
} else {
|
console.warn(`❌ No textarea found for field ${fieldName} in wrapper:`, fieldWrapper);
|
|
// Debug what's actually in the wrapper
|
const allTextareas = fieldWrapper.querySelectorAll('textarea');
|
const allInputs = fieldWrapper.querySelectorAll('input, textarea, select');
|
|
console.log('Debug - all textareas in wrapper:', allTextareas);
|
console.log('Debug - all inputs in wrapper:', allInputs);
|
console.log('Debug - wrapper innerHTML:', fieldWrapper.innerHTML);
|
}
|
}
|
|
|
|
/**
|
* 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 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) {
|
console.log(`Found Quill instance for ${fieldName}, setting content`);
|
// 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) {
|
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 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(',');
|
}
|
|
// Update taxonomy selector if present
|
if (fieldWrapper.dataset.fieldId && window.jvbSelector) {
|
window.jvbSelector.updateFieldFromInput(fieldWrapper.dataset.fieldId);
|
}
|
}
|
|
/**
|
* Populate user fields (similar to taxonomy)
|
*/
|
populateUserField(fieldWrapper, fieldName, fieldValue) {
|
// Similar logic to taxonomy fields
|
this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
|
}
|
|
/**
|
* 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) {
|
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 = {}) {
|
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}`;
|
|
// 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 || '');
|
}
|
}
|
}
|
|
// Initialize singleton
|
document.addEventListener('DOMContentLoaded', () => {
|
if (!window.jvbForm) {
|
window.jvbForm = new FormFields();
|
}
|
});
|
|
/*
|
USAGE EXAMPLES:
|
|
// Basic form with save callback
|
window.jvbForm.addForm(document.querySelector('#contact-form'), {
|
onSave: (changes) => {
|
console.log('Contact form saved:', changes);
|
// Send to API, show notification, etc.
|
},
|
autoSave: true,
|
saveDelay: 1000
|
});
|
|
// Form with multiple callbacks
|
window.jvbForm.addForm(document.querySelector('#profile-form'), {
|
onSave: (changes, formConfig) => {
|
// Handle save
|
window.jvbQueue.addToQueue({
|
endpoint: '/api/profile',
|
data: changes
|
});
|
},
|
onChange: (event, formConfig) => {
|
// Handle individual field changes
|
if (event.target.name === 'username') {
|
validateUsername(event.target.value);
|
}
|
},
|
onSubmit: (event, formConfig) => {
|
// Custom submit logic
|
event.preventDefault();
|
validateAndSubmitProfile(formConfig);
|
},
|
itemID: 123,
|
api: '/api/profile'
|
});
|
|
// Table row form
|
window.jvbForm.addForm(document.querySelector('#row-edit-form'), {
|
onSave: (changes, formConfig) => {
|
updateTableRow(formConfig.options.rowId, changes);
|
},
|
isRow: true,
|
rowId: 'row-456'
|
});
|
|
*/
|