/**
|
* Simplified autosave approach for FormController
|
* Leverages QueueManager for all server operations
|
*/
|
|
// In FormController class - replace the autosave-related methods with:
|
|
class FormController {
|
// ... existing constructor and init code ...
|
|
/**
|
* Initialize form tracking for autosave
|
*/
|
registerForm(formElement, options = {}) {
|
const formId = formElement.dataset.formId || `form_${Date.now()}`;
|
formElement.dataset.formId = formId;
|
|
const formConfig = {
|
element: formElement,
|
id: formId,
|
contentId: formElement.dataset.contentId || null, // For existing content
|
contentType: formElement.dataset.contentType || null,
|
options: {
|
autoSave: true,
|
saveDelay: this.autoSaveDefaults.delay,
|
endpoint: formElement.dataset.save || formElement.action,
|
...options
|
},
|
lastSnapshot: {}, // Last saved state
|
isDirty: false
|
};
|
|
// Take initial snapshot
|
formConfig.lastSnapshot = this.collectFormData(formElement);
|
|
// Initialize special fields
|
this.initializeFormFields(formElement, formConfig);
|
|
// Store form config
|
this.forms.set(formId, formConfig);
|
|
return formConfig;
|
}
|
|
/**
|
* Simplified change handler
|
*/
|
handleChange(event) {
|
const target = event.target;
|
const form = target.form || target.closest('form');
|
if (!form) return;
|
|
const formConfig = this.forms?.get(form.dataset.formId);
|
if (!formConfig) return;
|
|
// Check conditional fields (existing functionality)
|
const dependencies = formConfig.dependencies?.get(target.name);
|
if (dependencies) {
|
dependencies.forEach(dep => {
|
this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator);
|
});
|
}
|
|
// Schedule autosave if enabled
|
if (formConfig.options.autoSave && !form.dataset.noautosave) {
|
const delay = this.getDelayForField(target);
|
this.scheduleSave(formConfig, delay);
|
}
|
}
|
|
/**
|
* Get appropriate delay based on field type and context
|
*/
|
getDelayForField(field) {
|
// Text fields get longer delay for typing
|
if (field.type === 'text' || field.type === 'textarea') {
|
return this.autoSaveDefaults.typingDelay;
|
}
|
|
// Checkboxes, radios, selects get shorter delay
|
if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) {
|
return 1000;
|
}
|
|
// Default delay
|
return this.autoSaveDefaults.delay;
|
}
|
|
/**
|
* Simplified scheduleSave - just debounces the queue addition
|
*/
|
scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
|
const saveKey = `autosave_${formConfig.id}`;
|
|
this.debouncer.schedule(
|
saveKey,
|
() => this.queueAutosave(formConfig),
|
delay
|
);
|
}
|
|
/**
|
* Queue the autosave operation
|
*/
|
async queueAutosave(formConfig) {
|
const currentData = this.collectFormData(formConfig.element);
|
const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
|
|
// No changes? Don't save
|
if (Object.keys(changes).length === 0) return;
|
|
// For bulk edit forms, handle specially
|
if (formConfig.element.classList.contains('bulk-edit')) {
|
this.queueBulkSave(formConfig, changes);
|
return;
|
}
|
|
// Build the queue operation
|
const operation = {
|
endpoint: formConfig.options.endpoint || '/wp-json/jvb/v1/autosave',
|
method: formConfig.contentId ? 'PUT' : 'POST',
|
data: {
|
form_id: formConfig.id,
|
content_id: formConfig.contentId,
|
content_type: formConfig.contentType,
|
changes: changes,
|
full_data: currentData
|
},
|
title: `Autosaving ${formConfig.contentType || 'form'}`,
|
popup: null, // No popup for autosave
|
headers: {
|
'X-Autosave': 'true'
|
},
|
source: 'form',
|
formId: formConfig.id,
|
// For optimistic updates
|
localUpdate: formConfig.contentId ? {
|
action: 'update',
|
id: formConfig.contentId,
|
changes: changes
|
} : null,
|
dataStore: this.store
|
};
|
|
// Add to queue
|
const queueItem = await this.queue.addToQueue(operation);
|
|
if (queueItem) {
|
// Update snapshot to prevent re-saving same changes
|
formConfig.lastSnapshot = currentData;
|
formConfig.isDirty = false;
|
|
// Visual feedback
|
this.showFormStatus(formConfig.element, 'queued');
|
|
// Track the operation
|
this.trackOperation(formConfig, queueItem.id);
|
}
|
}
|
|
/**
|
* Handle bulk edit forms specially
|
*/
|
queueBulkSave(formConfig, changes) {
|
const selectedItems = formConfig.element.querySelectorAll('.bulk-item input:checked');
|
const itemIds = Array.from(selectedItems).map(cb => cb.value);
|
|
if (itemIds.length === 0) return;
|
|
const operation = {
|
endpoint: formConfig.options.endpoint || '/wp-json/jvb/v1/bulk-update',
|
method: 'PUT',
|
data: {
|
content_type: formConfig.contentType,
|
item_ids: itemIds,
|
changes: changes
|
},
|
title: `Updating ${itemIds.length} items`,
|
popup: `${itemIds.length} items queued for update`,
|
headers: {
|
'X-Bulk-Operation': 'true'
|
},
|
source: 'form',
|
formId: formConfig.id,
|
// Optimistic update for each item
|
localUpdate: {
|
action: 'bulk-update',
|
ids: itemIds,
|
changes: changes
|
},
|
dataStore: this.store
|
};
|
|
// Add to queue
|
const queueItem = this.queue.addToQueue(operation);
|
|
if (queueItem) {
|
formConfig.lastSnapshot = this.collectFormData(formConfig.element);
|
formConfig.isDirty = false;
|
this.showFormStatus(formConfig.element, 'queued');
|
}
|
}
|
|
/**
|
* Track operations for forms
|
*/
|
trackOperation(formConfig, operationId) {
|
if (!formConfig.operations) {
|
formConfig.operations = new Set();
|
}
|
formConfig.operations.add(operationId);
|
|
// Subscribe to queue updates for this operation
|
const unsubscribe = this.queue.subscribe((event, data) => {
|
if (data?.id !== operationId) return;
|
|
switch(event) {
|
case 'operation-status':
|
this.updateFormStatus(formConfig, data.status);
|
break;
|
|
case 'operation-completed':
|
this.handleSaveSuccess(formConfig, data);
|
formConfig.operations.delete(operationId);
|
unsubscribe();
|
break;
|
|
case 'operation-failed':
|
this.handleSaveFailure(formConfig, data);
|
formConfig.operations.delete(operationId);
|
unsubscribe();
|
break;
|
}
|
});
|
}
|
|
/**
|
* Update form status based on queue status
|
*/
|
updateFormStatus(formConfig, status) {
|
const statusMap = {
|
'queued': 'queued',
|
'uploading': 'saving',
|
'processing': 'saving',
|
'pending': 'saving',
|
'completed': 'saved',
|
'failed': 'error',
|
'failed_permanent': 'error'
|
};
|
|
this.showFormStatus(formConfig.element, statusMap[status] || status);
|
}
|
|
/**
|
* Handle successful save from queue
|
*/
|
handleSaveSuccess(formConfig, data) {
|
// Update content ID if this was a create operation
|
if (!formConfig.contentId && data.result?.id) {
|
formConfig.contentId = data.result.id;
|
formConfig.element.dataset.contentId = data.result.id;
|
}
|
|
// Reset dirty flag
|
formConfig.isDirty = false;
|
|
// Show success
|
this.showFormStatus(formConfig.element, 'saved');
|
|
// Notify subscribers
|
this.notify('form-saved', {
|
formId: formConfig.id,
|
contentId: formConfig.contentId,
|
data: data
|
});
|
}
|
|
/**
|
* Handle save failure from queue
|
*/
|
handleSaveFailure(formConfig, data) {
|
// Mark as dirty so it will retry
|
formConfig.isDirty = true;
|
|
// Show error
|
this.showFormStatus(formConfig.element, 'error');
|
|
// Notify subscribers
|
this.notify('form-save-failed', {
|
formId: formConfig.id,
|
error: data.lastError
|
});
|
}
|
|
/**
|
* Manual save (for submit button)
|
*/
|
async handleSubmit(event) {
|
const form = event.target;
|
if (!form.dataset.formId) return;
|
|
event.preventDefault();
|
|
const formConfig = this.forms.get(form.dataset.formId);
|
if (!formConfig) return;
|
|
// Force immediate save
|
this.debouncer.cancel(`autosave_${formConfig.id}`);
|
|
// Add to queue with higher priority
|
const currentData = this.collectFormData(form);
|
|
const operation = {
|
endpoint: formConfig.options.endpoint,
|
method: formConfig.contentId ? 'PUT' : 'POST',
|
data: {
|
content_id: formConfig.contentId,
|
content_type: formConfig.contentType,
|
...currentData
|
},
|
title: `Saving ${formConfig.contentType || 'form'}`,
|
popup: 'Saved successfully',
|
priority: 'high', // Process before autosaves
|
source: 'form',
|
formId: formConfig.id,
|
localUpdate: formConfig.contentId ? {
|
action: 'update',
|
id: formConfig.contentId,
|
changes: currentData
|
} : {
|
action: 'create',
|
data: currentData
|
},
|
dataStore: this.store
|
};
|
|
const queueItem = await this.queue.addToQueue(operation);
|
|
if (queueItem) {
|
formConfig.lastSnapshot = currentData;
|
this.trackOperation(formConfig, queueItem.id);
|
this.showFormStatus(form, 'saving');
|
}
|
}
|
|
/**
|
* Check if form has unsaved changes
|
*/
|
hasUnsavedChanges(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return false;
|
|
// Check if there are pending operations
|
if (formConfig.operations?.size > 0) return true;
|
|
// Check if current data differs from snapshot
|
const currentData = this.collectFormData(formConfig.element);
|
const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
|
|
return Object.keys(changes).length > 0;
|
}
|
|
/**
|
* Cleanup when form is closed/destroyed
|
*/
|
cleanupForm(formId) {
|
const formConfig = this.forms.get(formId);
|
if (!formConfig) return;
|
|
// Cancel any pending debounced saves
|
this.debouncer.cancel(`autosave_${formId}`);
|
|
// Check for unsaved changes
|
if (this.hasUnsavedChanges(formId)) {
|
// Could show a warning or auto-save
|
this.queueAutosave(formConfig);
|
}
|
|
// Clean up special fields
|
this.cleanupSpecialFields();
|
|
// Remove form config
|
this.forms.delete(formId);
|
}
|
}
|