/**
|
* 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
|
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 = window.jvbTabs.registerTab(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.jvbPopulate) {
|
this.populate = window.jvbPopulate.populate;
|
|
// Populate each field that exists in both schemas
|
Object.keys(currentData).forEach(fieldName => {
|
const fieldWrapper = form.querySelector(`[data-field="${fieldName}"]`);
|
if (fieldWrapper) {
|
const fieldValue = currentData[fieldName];
|
this.populate(fieldWrapper, fieldName, fieldValue);
|
}
|
});
|
|
// Announce changes
|
|
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';
|
}
|
|
/**
|
* 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();
|
}
|
});
|
});
|