form-functionality-library
Version:
A modular, flexible form functionality library for Webflow forms supporting single-step, multi-step, and branching forms
481 lines • 20.1 kB
JavaScript
/**
* Webflow Integration Module - v1.7.0 HARMONY EDITION
* Following Webflow's official patterns and best practices for elegant integration
*/
import { logVerbose } from './utils.js';
import { showError, clearError } from './errors.js';
let webflowInitialized = false;
let formElements = new Map();
let submissionInterceptors = new Set();
/**
* Initialize Webflow integration with comprehensive error handling
*/
export function initWebflowIntegration() {
if (webflowInitialized) {
logVerbose('[WebflowIntegration] Already initialized');
return;
}
logVerbose('[WebflowIntegration] 🤝 Starting Webflow Harmony Integration (v1.7.0)');
console.log('[WebflowIntegration] 🤝 Webflow Harmony Edition - Working WITH Webflow, not against it');
// Find all forms with data-form="multistep"
const multistepForms = document.querySelectorAll('form[data-form="multistep"]');
logVerbose(`[WebflowIntegration] Found ${multistepForms.length} multistep forms`);
console.log(`[WebflowIntegration] Found ${multistepForms.length} multistep forms`);
if (multistepForms.length === 0) {
logVerbose('[WebflowIntegration] ⚠️ No multistep forms found for integration');
console.log('[WebflowIntegration] ⚠️ No multistep forms found for integration');
return;
}
// Store form references and analyze each form
multistepForms.forEach((form, index) => {
const formElement = form;
const formId = formElement.id || `webflow-form-${index}`;
const method = formElement.method || 'get';
formElements.set(formId, formElement);
logVerbose(`[WebflowIntegration] 📋 Registered form: ${formId}`, {
method: method,
action: formElement.action || 'current-page',
elements: formElement.elements.length,
hasWebflowClass: formElement.classList.contains('w-form')
});
});
// Set up Webflow's official integration pattern
console.log('[WebflowIntegration] 🤝 Setting up Webflow.push() integration...');
setupWebflowNativeIntegration();
webflowInitialized = true;
logVerbose('[WebflowIntegration] ✅ Integration initialized successfully');
}
/**
* Setup Webflow's native integration using their official patterns (v1.7.0 HARMONY)
*/
function setupWebflowNativeIntegration() {
logVerbose('[WebflowIntegration] 🤝 Setting up Webflow native integration');
// Check if Webflow object exists
if (typeof window.Webflow === 'undefined') {
logVerbose('[WebflowIntegration] ⚠️ Webflow object not found, setting up fallback');
setupFallbackIntegration();
return;
}
// Use Webflow's official integration pattern
window.Webflow.push(function () {
console.log('[WebflowIntegration] 🎯 Webflow.push() executed - Setting up form validation');
// Set up validation for each multistep form using Webflow's pattern
formElements.forEach((formElement, formId) => {
setupWebflowFormValidation(formElement, formId);
});
});
logVerbose('[WebflowIntegration] ✅ Webflow.push() integration activated');
}
/**
* Setup Webflow-compatible form validation following their official patterns
*/
function setupWebflowFormValidation(form, formId) {
logVerbose(`[WebflowIntegration] 🎯 Setting up validation for form: ${formId}`);
// Use jQuery's submit event (Webflow's preferred method)
if (typeof window.$ !== 'undefined') {
window.$(form).submit(function () {
console.log(`[WebflowIntegration] 📝 Form submit intercepted: ${formId}`);
// Perform validation using our harmonious approach
const isValid = performWebflowCompatibleValidation(form);
if (!isValid) {
logVerbose(`[WebflowIntegration] ❌ Validation failed for: ${formId}`);
return false; // Prevent submission (Webflow's standard pattern)
}
logVerbose(`[WebflowIntegration] ✅ Validation passed for: ${formId}`);
return true; // Allow submission
});
}
else {
// Fallback to native events if jQuery not available
form.addEventListener('submit', function (event) {
if (!performWebflowCompatibleValidation(form)) {
event.preventDefault();
event.stopPropagation();
}
});
}
}
/**
* Fallback integration for when Webflow object is not available
*/
function setupFallbackIntegration() {
logVerbose('[WebflowIntegration] 🔄 Setting up fallback integration');
formElements.forEach((formElement, formId) => {
formElement.addEventListener('submit', function (event) {
if (!performWebflowCompatibleValidation(formElement)) {
event.preventDefault();
event.stopPropagation();
}
});
});
}
/**
* Perform Webflow-compatible validation following their simple patterns
* This replaces the complex validation logic with Webflow's elegant approach
*/
function performWebflowCompatibleValidation(form) {
logVerbose('[WebflowIntegration] 🔍 Performing Webflow-compatible validation');
// Clear existing errors first
clearFormErrors(form);
// Find required fields using Webflow's native 'required' attribute
const requiredFields = form.querySelectorAll('input[required], select[required], textarea[required]');
let isValid = true;
let firstErrorField = null;
requiredFields.forEach((field) => {
const input = field;
const fieldName = input.name || input.id || 'unnamed-field';
// Simple empty check (Webflow's approach)
if (!input.value || input.value.trim() === '') {
isValid = false;
// Show error using our harmonious error display
showWebflowCompatibleError(input, 'This field is required');
// Track first error for focus
if (!firstErrorField) {
firstErrorField = input;
}
}
});
// Focus first error field (Webflow pattern)
if (firstErrorField) {
firstErrorField.focus();
logVerbose('[WebflowIntegration] 🎯 Focused first error field');
}
logVerbose(`[WebflowIntegration] Validation result: ${isValid ? 'PASSED' : 'FAILED'}`);
return isValid;
}
/**
* Show error message using Webflow-compatible approach
*/
function showWebflowCompatibleError(input, message) {
const fieldName = input.name || input.id;
if (fieldName) {
// Use our harmonious showError function
showError(fieldName, message);
// Add Webflow-style error class to input
input.classList.add('input-error');
input.classList.remove('input-success');
}
}
/**
* Clear all form errors using simple approach
*/
function clearFormErrors(form) {
// Remove error classes from inputs
const inputs = form.querySelectorAll('input.input-error, select.input-error, textarea.input-error');
inputs.forEach(input => {
input.classList.remove('input-error');
});
// Clear error messages using our error system
const errorElements = form.querySelectorAll('.form_error-message[data-form="required"]');
errorElements.forEach(element => {
element.style.display = 'none';
});
}
/**
* Strategy 1: Direct native JavaScript event listeners (LEGACY - will be removed)
*/
function setupDirectEventListeners() {
logVerbose('[WebflowIntegration] 📡 Strategy 1: Setting up direct event listeners');
formElements.forEach((form, formId) => {
// Handle form submission events
form.addEventListener('submit', function (event) {
logVerbose(`[WebflowIntegration] 🚫 Direct submit intercepted: ${formId}`);
if (interceptFormSubmission(form, event)) {
logVerbose(`[WebflowIntegration] ✅ Direct validation passed: ${formId}`);
}
else {
logVerbose(`[WebflowIntegration] ❌ Direct validation failed, blocking: ${formId}`);
event.preventDefault();
event.stopPropagation();
}
});
// Handle beforesubmit for additional safety
form.addEventListener('beforesubmit', function (event) {
logVerbose(`[WebflowIntegration] 🚫 Before-submit intercepted: ${formId}`);
if (!interceptFormSubmission(form, event)) {
event.preventDefault();
event.stopPropagation();
}
});
logVerbose(`[WebflowIntegration] ✅ Direct listeners attached to: ${formId}`);
});
}
/**
* Strategy 2: Webflow.push() integration (if available)
*/
function setupWebflowPushIntegration() {
logVerbose('[WebflowIntegration] 📡 Strategy 2: Setting up Webflow.push() integration');
// Check if Webflow is available
if (typeof window.Webflow === 'undefined') {
logVerbose('[WebflowIntegration] ⚠️ Webflow object not available, skipping push integration');
return;
}
try {
window.Webflow.push(function () {
logVerbose('[WebflowIntegration] 🎯 Webflow.push() executed - setting up form hooks');
formElements.forEach((form, formId) => {
// Use native JavaScript instead of jQuery
form.addEventListener('submit', function (event) {
logVerbose(`[WebflowIntegration] 🚫 Webflow.push submit intercepted: ${formId}`);
if (!interceptFormSubmission(form, event)) {
event.preventDefault();
event.stopPropagation();
}
});
logVerbose(`[WebflowIntegration] ✅ Webflow.push listeners attached to: ${formId}`);
});
});
}
catch (error) {
logVerbose(`[WebflowIntegration] ❌ Webflow.push() failed: ${error}`);
}
}
/**
* Strategy 3: Submit button interception (GET form workaround)
*/
function setupSubmitButtonInterception() {
logVerbose('[WebflowIntegration] 📡 Strategy 3: Setting up submit button interception');
formElements.forEach((form, formId) => {
// Find submit buttons and submit-like elements
const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"], button:not([type]), [data-form="submit"]');
logVerbose(`[WebflowIntegration] Found ${submitButtons.length} submit buttons in form: ${formId}`);
submitButtons.forEach((button, index) => {
button.addEventListener('click', function (event) {
logVerbose(`[WebflowIntegration] 🚫 Submit button clicked: ${formId}-button-${index}`);
// Perform validation before allowing the click to proceed
if (!performFinalValidation(form)) {
logVerbose(`[WebflowIntegration] ❌ Button click validation failed: ${formId}`);
event.preventDefault();
event.stopPropagation();
}
else {
logVerbose(`[WebflowIntegration] ✅ Button click validation passed: ${formId}`);
}
});
});
// Also look for Webflow's form submission triggers
const webflowSubmits = form.querySelectorAll('.w-button[type="submit"], .w-button[form]');
webflowSubmits.forEach((button, index) => {
button.addEventListener('click', function (event) {
logVerbose(`[WebflowIntegration] 🚫 Webflow submit button clicked: ${formId}-wf-${index}`);
if (!performFinalValidation(form)) {
event.preventDefault();
event.stopPropagation();
}
else {
logVerbose(`[WebflowIntegration] ✅ Webflow submit validation passed: ${formId}`);
}
});
});
});
}
/**
* Strategy 4: Form action override (final safety net)
*/
function setupFormActionOverride() {
logVerbose('[WebflowIntegration] 📡 Strategy 4: Setting up form action override');
formElements.forEach((form, formId) => {
// Store original action
const originalAction = form.action;
// Override form action temporarily
Object.defineProperty(form, 'action', {
get: function () {
return originalAction;
},
set: function (value) {
logVerbose(`[WebflowIntegration] 🚫 Form action change intercepted: ${formId}`);
// Perform validation before allowing action change
if (performFinalValidation(form)) {
logVerbose(`[WebflowIntegration] ✅ Action change validation passed: ${formId}`);
return value;
}
else {
logVerbose(`[WebflowIntegration] ❌ Action change validation failed: ${formId}`);
return '#'; // Block the action change
}
}
});
});
}
/**
* Central form submission interception logic
*/
function interceptFormSubmission(form, event) {
const formId = form.id || 'unknown';
logVerbose(`[WebflowIntegration] 🔍 Intercepting submission for: ${formId}`);
return performFinalValidation(form);
}
/**
* Perform final validation before form submission (enhanced v1.6.1)
*/
function performFinalValidation(form) {
const formId = form.id || 'unknown';
logVerbose(`[WebflowIntegration] 🔍 Performing final validation for: ${formId}`);
// Clear any existing errors first
clearAllFormErrors(form);
// Get all required fields in the entire form (supports both required and data-required)
const requiredFields = form.querySelectorAll('input[required], input[data-required], select[required], select[data-required], textarea[required], textarea[data-required]');
logVerbose(`[WebflowIntegration] Found ${requiredFields.length} required fields in form`);
if (requiredFields.length === 0) {
logVerbose(`[WebflowIntegration] ✅ No required fields found, validation passed: ${formId}`);
return true;
}
let hasErrors = false;
const errors = [];
// Validate each required field
requiredFields.forEach((field, index) => {
const input = field;
const fieldName = input.name || input.id || `field-${index}`;
const value = getFieldValue(input);
logVerbose(`[WebflowIntegration] 🔍 Validating field ${index + 1}: ${fieldName}`, {
type: input.type,
value: Array.isArray(value) ? `[${value.length} items]` : value,
isEmpty: isFieldEmpty(value)
});
if (isFieldEmpty(value)) {
const errorMessage = getCustomErrorMessage(input) || 'This field is required';
errors.push({ field: input, message: errorMessage });
hasErrors = true;
logVerbose(`[WebflowIntegration] ❌ Required field is empty: ${fieldName}`);
}
else {
logVerbose(`[WebflowIntegration] ✅ Required field is valid: ${fieldName}`);
}
});
// Show errors if any
if (hasErrors) {
logVerbose(`[WebflowIntegration] 🚫 Validation failed - ${errors.length} fields with errors in form: ${formId}`);
errors.forEach(({ field, message }) => {
const fieldName = field.name || field.id || 'unknown';
showError(fieldName, message);
logVerbose(`[WebflowIntegration] 📝 Showing error for field: ${fieldName} - "${message}"`);
});
// Focus first error field
if (errors.length > 0) {
focusField(errors[0].field);
logVerbose(`[WebflowIntegration] 🎯 Focused first error field: ${errors[0].field.name || errors[0].field.id}`);
}
return false;
}
logVerbose(`[WebflowIntegration] ✅ All fields validated successfully for form: ${formId}`);
return true;
}
/**
* Get field value based on input type
*/
function getFieldValue(input) {
switch (input.type) {
case 'checkbox':
return input.checked ? [input.value] : [];
case 'radio':
const radioGroup = document.querySelectorAll(`input[name="${input.name}"]:checked`);
return Array.from(radioGroup).map((radio) => radio.value);
default:
return input.value;
}
}
/**
* Check if field value is empty
*/
function isFieldEmpty(value) {
if (Array.isArray(value)) {
return value.length === 0;
}
return !value || value.trim() === '';
}
/**
* Get custom error message for field
*/
function getCustomErrorMessage(input) {
// Look for custom error message in parent container
const fieldWrapper = input.closest('.form-field_wrapper') || input.parentElement;
if (!fieldWrapper)
return null;
// PRIORITY 1: Look for data-form="required" elements first
const customErrorElement = fieldWrapper.querySelector('[data-form="required"]');
if (customErrorElement && customErrorElement.textContent) {
const customText = customErrorElement.textContent.trim();
if (customText) {
logVerbose(`[WebflowIntegration] Found custom error message: ${customText}`);
return customText;
}
}
// PRIORITY 2: Look for .form_error-message elements
const errorElement = fieldWrapper.querySelector('.form_error-message');
if (errorElement && errorElement.textContent) {
const customText = errorElement.textContent.trim();
if (customText) {
logVerbose(`[WebflowIntegration] Found fallback error message: ${customText}`);
return customText;
}
}
return null;
}
/**
* Clear all form errors
*/
function clearAllFormErrors(form) {
const errorElements = form.querySelectorAll('.form_error-message, [data-form="required"]');
errorElements.forEach((element) => {
const input = findRelatedInput(element);
if (input) {
const fieldName = input.name || input.id || 'unknown';
clearError(fieldName);
}
});
}
/**
* Find input related to error element
*/
function findRelatedInput(errorElement) {
const fieldWrapper = errorElement.closest('.form-field_wrapper') || errorElement.parentElement;
if (!fieldWrapper)
return null;
return fieldWrapper.querySelector('input, select, textarea');
}
/**
* Focus a field
*/
function focusField(field) {
try {
if ('focus' in field && typeof field.focus === 'function') {
field.focus();
logVerbose(`[WebflowIntegration] Focused field: ${field.name || field.id}`);
}
}
catch (error) {
logVerbose(`[WebflowIntegration] Could not focus field: ${error}`);
}
}
/**
* Check if Webflow integration is available
*/
export function isWebflowAvailable() {
return typeof window.Webflow !== 'undefined';
}
/**
* Get comprehensive integration status (v1.6.1)
*/
export function getWebflowIntegrationStatus() {
const formDetails = Array.from(formElements.entries()).map(([id, form]) => ({
id,
method: form.method || 'get',
action: form.action || 'current-page',
requiredFields: form.querySelectorAll('input[required], input[data-required], select[required], select[data-required], textarea[required], textarea[data-required]').length,
hasWebflowClass: form.classList.contains('w-form')
}));
return {
initialized: webflowInitialized,
webflowAvailable: isWebflowAvailable(),
formsRegistered: formElements.size,
interceptorsActive: submissionInterceptors.size,
formDetails,
strategies: {
directListeners: true, // Always attempted
webflowPush: typeof window.Webflow !== 'undefined',
buttonInterception: true, // Always attempted
actionOverride: true // Always attempted
}
};
}
//# sourceMappingURL=webflowIntegration.js.map