/** * SEO Admin Page Controller * Handles schema type switching, form initialization, and tabs * Works with FormController for unified form handling */ class SchemaManager { constructor() { this.formController = null; this.tabsInstance = null; this.queue = window.jvbQueue; this.a11y = window.jvbA11y; this.init(); } init() { // Initialize FormController if (window.jvbForm && !window.formController) { this.formController = new window.jvbForm(); window.formController = this.formController; } else if (window.formController) { this.formController = window.formController; } // Initialize main Tabs (they're outside forms, so FormController won't handle them) if (window.jvbTabs) { const tabContainer = document.querySelector('.jvb-seo-admin'); if (tabContainer) { this.tabsInstance = new window.jvbTabs(tabContainer); } } // Subscribe to FormController events if (this.formController) { this.formController.subscribe((event, data) => { if (event === 'form-submit') { this.handleFormSubmit(data); } }); } // Subscribe to Queue events if (this.queue) { this.queue.subscribe((event, data) => { if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'seo') return; if (event === 'operation-completed') { this.handleQueueSuccess(event, data); } else if (event === 'operation-failed-permanent') { this.handleQueueFailure(event, data); } }); } // Initialize all SEO forms this.initializeForms(); // Add preserved field styling this.addPreservedFieldStyles(); } /** * Initialize all SEO forms */ initializeForms() { const forms = document.querySelectorAll('form[data-save="seo"]'); forms.forEach(form => { // Register with FormController if (this.formController) { this.formController.registerForm(form, { endpoint: 'seo', autosave: false, formStatus: false }); } // Set up type switching this.initializeTypeSwitch(form); // Set up reset button const resetBtn = form.querySelector('[data-action="reset"]'); if (resetBtn) { resetBtn.addEventListener('click', () => this.handleReset(form)); } }); } /** * Handle form submission via Queue */ handleFormSubmit(data) { const form = data.config.element; const context = form.dataset.content; const formData = data.fullData; // Build operation for queue const operation = { endpoint: 'seo', headers: { 'X-WP-Nonce': window.auth.getNonce() }, data: { context: context, action: 'save', ...formData }, popup: 'Saving SEO configuration', title: `Saving ${context} settings` }; this.queue.addToQueue(operation); } /** * Handle reset button */ async handleReset(form) { const context = form.dataset.content; if (!confirm('Reset to default settings? This cannot be undone.')) { return; } const operation = { endpoint: 'seo', headers: { 'X-WP-Nonce': window.auth.getNonce() }, data: { context: context, action: 'reset' }, popup: 'Resetting configuration', title: `Resetting ${context} to defaults` }; this.queue.addToQueue(operation); } /** * Handle queue success */ handleQueueSuccess(event, data) { console.log('SEO save successful:', data); if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce('Configuration saved successfully'); } // If this was a reset, reload the form data if (data.operation?.data?.action === 'reset' && data.response?.schema) { this.reloadFormData(data.operation.data.context, data.response); } } /** * Handle queue failure */ handleQueueFailure(event, data) { console.error('SEO operation failed permanently:', data); if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce(`Error: ${data.error_message || 'Operation failed'}`); } } /** * Reload form data after reset */ reloadFormData(context, response) { const form = document.querySelector(`form[data-content="${context}"]`); if (!form) return; const schema = response.schema || {}; // Update form fields with reset values Object.keys(schema).forEach(key => { const field = form.querySelector(`[name="${key}"]`); if (field) { if (field.type === 'checkbox') { field.checked = !!schema[key]; } else { field.value = schema[key] || ''; } } }); if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce('Form reset to defaults'); } } /** * Initialize schema type switching for a form */ initializeTypeSwitch(form) { const typeSelect = form.querySelector('select[name="type"]'); if (!typeSelect) return; // Handle type change with confirmation typeSelect.addEventListener('change', (e) => { const oldType = form.dataset.currentType || typeSelect.dataset.initialValue; const newType = e.target.value; // If types are the same, no need to confirm if (oldType === newType) return; // Show confirmation dialog this.confirmTypeChange(form, typeSelect, oldType, newType); }); // Store initial type for reference typeSelect.dataset.initialValue = typeSelect.value; form.dataset.currentType = typeSelect.value; } /** * Confirm type change with user */ confirmTypeChange(form, typeSelect, oldType, newType) { // Get current form values const currentValues = {}; const formData = new FormData(form); for (let [key, value] of formData.entries()) { if (key !== 'type' && value && value !== '') { currentValues[key] = value; } } // Get template for new type to check which fields will be preserved const newTemplate = window.getTemplate(`seo-${newType}`); if (!newTemplate) { console.error('No template found for type:', newType); typeSelect.value = oldType; return; } // Extract base field names from current values // Handles both regular fields and repeater fields (fieldName:index:subField) const getBaseFieldName = (fieldName) => { return fieldName.split(':')[0]; }; const currentBaseFields = new Set( Object.keys(currentValues).map(getBaseFieldName) ); // Get base field names from new template const newFieldElements = newTemplate.querySelectorAll('[data-field]'); const newBaseFields = new Set( Array.from(newFieldElements).map(el => el.dataset.field) ); // If no data-field attributes, fall back to name attributes if (newBaseFields.size === 0) { const nameElements = newTemplate.querySelectorAll('[name]'); Array.from(nameElements).forEach(el => { newBaseFields.add(getBaseFieldName(el.getAttribute('name'))); }); } // Determine preserved and lost fields const preservedFields = [...currentBaseFields].filter(field => newBaseFields.has(field)); const lostFields = [...currentBaseFields].filter(field => !newBaseFields.has(field)); // Build confirmation message let message = `Change schema type from ${oldType} to ${newType}?\n\n`; if (preservedFields.length > 0) { message += `✓ ${preservedFields.length} field value(s) will be preserved:\n`; message += preservedFields.map(f => ` • ${f}`).join('\n'); message += '\n\n'; } if (lostFields.length > 0) { message += `⚠ ${lostFields.length} field value(s) will be lost:\n`; message += lostFields.map(f => ` • ${f}`).join('\n'); } // Show confirmation if (confirm(message)) { this.handleTypeChange(form, typeSelect, newType); } else { // User cancelled - revert select typeSelect.value = oldType; if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce('Type change cancelled'); } } } /** * Handle schema type change */ handleTypeChange(form, typeSelect, newType) { const oldType = form.dataset.currentType || typeSelect.dataset.initialValue; // Collect current form data as structured object // Group repeater fields by base name const currentData = this.collectFormData(form); // Get template for new type const newFields = window.getTemplate(`seo-${newType}`); if (!newFields) { console.error('No template found for type:', newType); return; } // Replace the field container const oldContainer = form.querySelector('.seo-' + oldType); if (oldContainer) { // Insert new fields oldContainer.parentNode.insertBefore(newFields, oldContainer); // Remove old container oldContainer.remove(); } // Update current type tracking form.dataset.currentType = newType; // Use PopulateForm to properly populate all fields including repeaters if (window.jvbPopulateForm) { const populator = new window.jvbPopulateForm(); const preservedFields = []; // Populate each field that exists in both schemas Object.keys(currentData).forEach(fieldName => { const fieldWrapper = form.querySelector(`[data-field="${fieldName}"]`); if (fieldWrapper) { const fieldType = this.getFieldType(fieldWrapper); const fieldValue = currentData[fieldName]; // Use PopulateForm's methods for complex fields if (fieldType === 'repeater' && Array.isArray(fieldValue)) { populator.populateRepeaterField(fieldWrapper, fieldName, fieldValue); preservedFields.push(fieldName); } else if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') { // Simple field - populate directly const field = fieldWrapper.querySelector(`[name="${fieldName}"]`) || fieldWrapper.querySelector(`[name^="${fieldName}"]`); if (field) { this.populateSimpleField(field, fieldValue); preservedFields.push(fieldName); } } } }); // Announce changes if (preservedFields.length > 0) { const message = `Schema type changed to ${newType}. Preserved ${preservedFields.length} field value(s).`; console.log(message); if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce(message); } } else { const message = `Schema type changed to ${newType}.`; if (this.a11y && typeof this.a11y.announce === 'function') { this.a11y.announce(message); } } } } /** * Collect form data into structured object * Handles repeater fields by grouping them */ collectFormData(form) { const data = {}; const formData = new FormData(form); for (let [key, value] of formData.entries()) { if (key === 'type' || key === 'context') continue; // Check if this is a repeater field (format: fieldName:index:subField) if (key.includes(':')) { const parts = key.split(':'); const baseField = parts[0]; const index = parseInt(parts[1]); const subField = parts[2]; // Initialize repeater array if needed if (!data[baseField]) { data[baseField] = []; } // Initialize row object if needed if (!data[baseField][index]) { data[baseField][index] = {}; } // Store the value data[baseField][index][subField] = value; } else { // Regular field data[key] = value; } } return data; } /** * Get field type from wrapper element */ getFieldType(fieldWrapper) { if (fieldWrapper.classList.contains('repeater')) { return 'repeater'; } // Add other field type checks as needed return 'text'; } /** * Populate a simple field with value */ populateSimpleField(field, value) { if (field.type === 'checkbox') { field.checked = value === '1' || value === 'true' || value === true; } else if (field.tagName === 'SELECT') { setTimeout(() => { field.value = value; }, 10); } else { field.value = value; } // Visual feedback field.classList.add('value-preserved'); setTimeout(() => field.classList.remove('value-preserved'), 2000); } /** * Add CSS for preserved field indication */ addPreservedFieldStyles() { const style = document.createElement('style'); style.textContent = ` .value-preserved { background-color: #e7f5e7 !important; transition: background-color 0.3s ease; } `; document.head.appendChild(style); } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', async function () { window.auth.subscribe((event) => { if (event === 'auth-loaded') { window.jvbSchema = new SchemaManager(); } }); });