landestreffen-webseite/plugins/form/assets/xhr-submitter.js

461 lines
18 KiB
JavaScript

/**
* Grav Form XHR Submitter
*
* A modular system for handling form submissions via XMLHttpRequest (AJAX).
* Features include content replacement, captcha handling, and error management.
*/
(function() {
'use strict';
// Main namespace
window.GravFormXHR = {};
/**
* Core Module - Contains configuration and utility functions
*/
const Core = {
config: {
debug: false,
enableLoadingIndicator: false
},
/**
* Configure global settings
* @param {Object} options - Configuration options
*/
configure: function(options) {
Object.assign(this.config, options);
},
/**
* Logger utility
* @param {string} message - Message to log
* @param {string} level - Log level ('log', 'warn', 'error')
*/
log: function(message, level = 'log') {
if (!this.config.debug) return;
const validLevels = ['log', 'warn', 'error'];
const finalLevel = validLevels.includes(level) ? level : 'log';
console[finalLevel](`[GravFormXHR] ${message}`);
},
/**
* Display an error message within a target element
* @param {HTMLElement} target - The element to display the error in
* @param {string} message - The error message
*/
displayError: function(target, message) {
const errorMsgContainer = target.querySelector('.form-messages') || target;
const errorMsg = document.createElement('div');
errorMsg.className = 'form-message error';
errorMsg.textContent = message;
errorMsgContainer.insertBefore(errorMsg, errorMsgContainer.firstChild);
}
};
/**
* DOM Module - Handles DOM manipulation and form tracking
*/
const DOM = {
/**
* Find a form wrapper by formId
* @param {string} formId - ID of the form
* @returns {HTMLElement|null} - The wrapper element or null
*/
getFormWrapper: function(formId) {
const wrapperId = formId + '-wrapper';
return document.getElementById(wrapperId);
},
/**
* Add or remove loading indicators
* @param {HTMLElement} form - The form element
* @param {HTMLElement} wrapper - The wrapper element
* @param {boolean} isLoading - Whether to add or remove loading classes
*/
updateLoadingState: function(form, wrapper, isLoading) {
if (!Core.config.enableLoadingIndicator) return;
if (isLoading) {
wrapper.classList.add('loading');
form.classList.add('submitting');
} else {
wrapper.classList.remove('loading');
form.classList.remove('submitting');
}
},
/**
* Update form content with server response
* @param {string} responseText - Server response HTML
* @param {string} wrapperId - ID of the wrapper to update
* @param {string} formId - ID of the original form
*/
updateFormContent: function(responseText, wrapperId, formId) {
const wrapperElement = document.getElementById(wrapperId);
if (!wrapperElement) {
console.error(`Cannot update content: Wrapper #${wrapperId} not found`);
return;
}
Core.log(`Updating content for wrapper: ${wrapperId}`);
// Parse response
const tempDiv = document.createElement('div');
try {
tempDiv.innerHTML = responseText;
} catch (e) {
console.error(`Error parsing response HTML for wrapper: ${wrapperId}`, e);
Core.displayError(wrapperElement, 'An error occurred processing the server response.');
return;
}
try {
this._updateWrapperContent(tempDiv, wrapperElement, wrapperId, formId);
this._reinitializeUpdatedForm(wrapperElement, formId);
} catch (e) {
console.error(`Error during content update for wrapper ${wrapperId}:`, e);
Core.displayError(wrapperElement, 'An error occurred updating the form content.');
}
},
/**
* Update wrapper content based on response parsing strategy
* @private
*/
_updateWrapperContent: function(tempDiv, wrapperElement, wrapperId, formId) {
// Strategy 1: Look for matching wrapper ID in response
const newWrapperElement = tempDiv.querySelector('#' + wrapperId);
if (newWrapperElement) {
wrapperElement.innerHTML = newWrapperElement.innerHTML;
Core.log(`Update using newWrapperElement.innerHTML SUCCESSFUL for wrapper: ${wrapperId}`);
return;
}
// Strategy 2: Look for matching form ID in response
const hasMatchingForm = tempDiv.querySelector('#' + formId);
if (hasMatchingForm) {
Core.log(`Wrapper element #${wrapperId} not found in XHR response, but found matching form. Using entire response.`);
wrapperElement.innerHTML = tempDiv.innerHTML;
return;
}
// Strategy 3: Look for toast messages
const hasToastMessages = tempDiv.querySelector('.toast');
if (hasToastMessages) {
Core.log('Found toast messages in response. Updating wrapper with the response.');
wrapperElement.innerHTML = tempDiv.innerHTML;
return;
}
// Fallback: Use entire response with warning
Core.log('No matching content found in response. Response may not be valid for this wrapper.', 'warn');
wrapperElement.innerHTML = tempDiv.innerHTML;
},
/**
* Reinitialize updated form and its components
* @private
*/
_reinitializeUpdatedForm: function(wrapperElement, formId) {
const updatedForm = wrapperElement.querySelector('#' + formId);
if (updatedForm) {
Core.log(`Re-running initialization for form ${formId} after update`);
// First reinitialize any captchas
CaptchaManager.reinitializeAll(updatedForm);
// Trigger mutation._grav event for Dropzone and other field reinitializations
setTimeout(() => {
Core.log('Triggering mutation._grav event for field reinitialization');
// Trigger using jQuery if available (preferred method for compatibility)
if (typeof jQuery !== 'undefined') {
jQuery('body').trigger('mutation._grav', [wrapperElement]);
} else {
// Fallback: dispatch native custom event
const event = new CustomEvent('mutation._grav', {
detail: { target: wrapperElement },
bubbles: true
});
document.body.dispatchEvent(event);
}
}, 0);
// Then re-attach the XHR listener
setTimeout(() => {
FormHandler.setupListener(formId);
}, 10);
} else {
// Check if this was a successful submission with just a message
const hasSuccessMessage = wrapperElement.querySelector('.toast-success, .form-success');
if (hasSuccessMessage) {
Core.log('No form found after update, but success message detected. This appears to be a successful submission.');
} else {
console.warn(`Could not find form #${formId} inside the updated wrapper after update. Cannot re-attach listener/initializers.`);
}
}
}
};
/**
* XHR Module - Handles XMLHttpRequest operations
*/
const XHRManager = {
/**
* Send form data via XHR
* @param {HTMLFormElement} form - The form to submit
*/
sendFormData: function(form) {
const formId = form.id;
const wrapperId = formId + '-wrapper';
const wrapperElement = DOM.getFormWrapper(formId);
if (!wrapperElement) {
console.error(`XHR submission: Target wrapper element #${wrapperId} not found on the page! Cannot proceed.`);
form.innerHTML = '<p class="form-message error">Error: Form wrapper missing. Cannot update content.</p>';
return;
}
Core.log(`Initiating XHR submission for form: ${formId}, targeting wrapper: ${wrapperId}`);
DOM.updateLoadingState(form, wrapperElement, true);
const xhr = new XMLHttpRequest();
xhr.open(form.getAttribute('method') || 'POST', form.getAttribute('action') || window.location.href);
// Set Headers
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Grav-Form-XHR', 'true');
// Success handler
xhr.onload = () => {
Core.log(`XHR request completed for form: ${formId}, Status: ${xhr.status}`);
DOM.updateLoadingState(form, wrapperElement, false);
if (xhr.status >= 200 && xhr.status < 300) {
DOM.updateFormContent(xhr.responseText, wrapperId, formId);
} else {
Core.log(`Form submission failed for form: ${formId}, HTTP Status: ${xhr.status} ${xhr.statusText}`, 'error');
Core.displayError(wrapperElement, `An error occurred during submission (Status: ${xhr.status}). Please check the form and try again.`);
}
};
// Network error handler
xhr.onerror = () => {
Core.log(`Form submission failed due to network error for form: ${formId}`, 'error');
DOM.updateLoadingState(form, wrapperElement, false);
Core.displayError(wrapperElement, 'A network error occurred. Please check your connection and try again.');
};
// Prepare and send data
try {
const formData = new FormData(form);
const urlEncodedData = new URLSearchParams(formData).toString();
Core.log(`Sending XHR request for form: ${formId} with custom header X-Grav-Form-XHR`);
xhr.send(urlEncodedData);
} catch (e) {
Core.log(`Error preparing or sending XHR request for form: ${formId}: ${e.message}`, 'error');
DOM.updateLoadingState(form, wrapperElement, false);
Core.displayError(wrapperElement, 'An unexpected error occurred before sending the form.');
}
}
};
/**
* CaptchaManager - Handles captcha registration and initialization
*/
const CaptchaManager = {
providers: {},
/**
* Register a captcha provider
* @param {string} name - Provider name
* @param {object} provider - Provider object with init and reset methods
*/
register: function(name, provider) {
this.providers[name] = provider;
Core.log(`Registered captcha provider: ${name}`);
},
/**
* Get a provider by name
* @param {string} name - Provider name
* @returns {object|null} Provider object or null if not found
*/
getProvider: function(name) {
return this.providers[name] || null;
},
/**
* Get all registered providers
* @returns {object} Object containing all providers
*/
getProviders: function() {
return this.providers;
},
/**
* Reinitialize all captchas in a form
* @param {HTMLFormElement} form - Form element containing captchas
*/
reinitializeAll: function(form) {
if (!form || !form.id) return;
const formId = form.id;
const containers = form.querySelectorAll('[data-captcha-provider]');
containers.forEach(container => {
const providerName = container.dataset.captchaProvider;
Core.log(`Found captcha container for provider: ${providerName} in form: ${formId}`);
const provider = this.getProvider(providerName);
if (provider && typeof provider.reset === 'function') {
setTimeout(() => {
try {
provider.reset(container, form);
Core.log(`Successfully reset ${providerName} captcha in form: ${formId}`);
} catch (e) {
console.error(`Error resetting ${providerName} captcha:`, e);
}
}, 0);
} else {
console.warn(`Could not reset captcha provider "${providerName}" - provider not registered or missing reset method`);
}
});
}
};
/**
* FormHandler - Handles form submission and event listeners
*/
const FormHandler = {
/**
* Submit a form via XHR
* @param {HTMLFormElement} form - Form to submit
*/
submitForm: function(form) {
if (!form || !form.id) {
console.error('submitForm called with invalid form element or form missing ID.');
return;
}
XHRManager.sendFormData(form);
},
/**
* Set up XHR submission listener for a form
* @param {string} formId - ID of the form
*/
setupListener: function(formId) {
setTimeout(() => {
const form = document.getElementById(formId);
if (!form) {
Core.log(`XHR Setup (delayed): Form with ID "${formId}" not found.`, 'warn');
return;
}
// Remove stale marker from previous runs
delete form.dataset.directXhrListenerAttached;
// Check if any captcha provider is handling the submission
const captchaContainer = form.querySelector('[data-captcha-provider][data-intercepts-submit="true"]');
if (!captchaContainer) {
// No intercepting captcha found, attach direct listener
this._attachDirectListener(form);
} else {
// Captcha will intercept, don't attach direct listener
const providerName = captchaContainer.dataset.captchaProvider;
Core.log(`XHR listener deferred: ${providerName} should intercept submit for form: ${formId}`);
// Ensure no stale listener marker remains
delete form.dataset.directXhrListenerAttached;
}
}, 0);
},
/**
* Attach a direct submit event listener to a form
* @private
* @param {HTMLFormElement} form - Form element
*/
_attachDirectListener: function(form) {
// Only proceed if XHR is enabled for this form
if (form.dataset.xhrEnabled !== 'true') {
Core.log(`XHR not enabled for form: ${form.id}. Skipping direct listener attachment.`);
return;
}
// Check if we already attached a listener
if (form.dataset.directXhrListenerAttached === 'true') {
Core.log(`Direct XHR listener already attached for form: ${form.id}`);
return;
}
const directXhrSubmitHandler = (event) => {
Core.log(`Direct XHR submit handler triggered for form: ${form.id}`);
event.preventDefault();
FormHandler.submitForm(form);
};
Core.log(`Attaching direct XHR listener for form: ${form.id}`);
form.addEventListener('submit', directXhrSubmitHandler);
form.dataset.directXhrListenerAttached = 'true';
}
};
// Initialize basic built-in captcha handlers
// Other providers will register themselves via separate handler JS files
const initializeBasicCaptchaHandlers = function() {
// Basic captcha handler (image refresh etc.)
CaptchaManager.register('basic-captcha', {
reset: function(container, form) {
const formId = form.id;
const captchaImg = container.querySelector('img');
const captchaInput = container.querySelector('input[type="text"]');
if (captchaImg) {
// Add a timestamp to force image reload
const timestamp = new Date().getTime();
const imgSrc = captchaImg.src.split('?')[0] + '?t=' + timestamp;
captchaImg.src = imgSrc;
// Clear any existing input
if (captchaInput) {
captchaInput.value = '';
}
Core.log(`Reset basic-captcha for form: ${formId}`);
}
}
});
};
// Initialize basic captcha handlers
initializeBasicCaptchaHandlers();
// --- Expose Public API ---
// Core configuration
window.GravFormXHR.configure = Core.configure.bind(Core);
// Form submission
window.GravFormXHR.submit = FormHandler.submitForm.bind(FormHandler);
window.GravFormXHR.setupListener = FormHandler.setupListener.bind(FormHandler);
// Captcha management
window.GravFormXHR.captcha = CaptchaManager;
// Legacy support
window.GravFormXHRSubmitters = {submit: FormHandler.submitForm.bind(FormHandler)};
window.attachFormSubmitListener = FormHandler.setupListener.bind(FormHandler);
})();