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 ? `` : ''; toolbar.id = `toolbar-${textarea.id}`; toolbar.innerHTML = ` ${image} `; 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 = ` `; 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' }); */