form-functionality-library
Version:
A modular, flexible form functionality library for Webflow forms supporting single-step, multi-step, and branching forms
283 lines • 10.8 kB
JavaScript
/**
* Enhanced Error Handling Module - Form Field Wrapper Standardization
* Provides robust error message handling that works with inconsistent HTML structures
* Version: 1.8.0 - Form Wrapper Standardization
*/
import { logVerbose } from './utils';
import { addClass, removeClass } from './utils';
const CSS_CLASSES = {
ERROR_FIELD: 'error-field',
ACTIVE_ERROR: 'active-error'
};
// Map to store enhanced error configurations
const enhancedErrorConfigs = new Map();
/**
* Enhanced error element finder that handles inconsistent HTML structures
*/
function findOrCreateErrorElement(element, fieldName) {
const parentWrapper = element.closest('.form-field_wrapper, .multi-form_input-field, .form_input-phone-wrapper');
if (!parentWrapper) {
logVerbose(`No suitable parent wrapper found for field: ${fieldName}`);
return createErrorElement(element, fieldName);
}
// Try multiple selectors in order of preference
const selectors = [
'.form_error-message[data-form="required"]',
'.form_error-message',
'[data-form="required"]'
];
let errorElement = null;
// Search within the wrapper
for (const selector of selectors) {
errorElement = parentWrapper.querySelector(selector);
if (errorElement) {
logVerbose(`Found error element using selector: ${selector}`, { fieldName });
break;
}
}
// If no error element found, create one
if (!errorElement) {
logVerbose(`No error element found for field: ${fieldName}, creating new one`);
errorElement = createErrorElement(element, fieldName);
}
// Ensure the error element has the required attributes
standardizeErrorElement(errorElement);
return errorElement;
}
/**
* Create a new error element with proper structure
*/
function createErrorElement(inputElement, fieldName) {
const errorElement = document.createElement('div');
errorElement.className = 'form_error-message';
errorElement.setAttribute('data-form', 'required');
errorElement.setAttribute('data-field', fieldName);
errorElement.textContent = 'This field is required';
// Hide by default
errorElement.style.display = 'none';
// Find the best insertion point
const insertionPoint = findInsertionPoint(inputElement);
insertionPoint.appendChild(errorElement);
logVerbose(`Created new error element for field: ${fieldName}`, {
insertedAfter: insertionPoint.tagName,
parentClasses: insertionPoint.className
});
return errorElement;
}
/**
* Find the best place to insert a new error element
*/
function findInsertionPoint(inputElement) {
// First preference: after the input's immediate wrapper if it exists
const immediateWrapper = inputElement.parentElement;
if (immediateWrapper && (immediateWrapper.classList.contains('multi-form_input-field') ||
immediateWrapper.classList.contains('form_input-phone-wrapper'))) {
return immediateWrapper.parentElement || immediateWrapper;
}
// Second preference: after the input itself
return inputElement.parentElement || inputElement;
}
/**
* Ensure error element has all required attributes and classes
*/
function standardizeErrorElement(errorElement) {
if (!errorElement.hasAttribute('data-form')) {
errorElement.setAttribute('data-form', 'required');
}
if (!errorElement.classList.contains('form_error-message')) {
errorElement.classList.add('form_error-message');
}
}
/**
* Enhanced error showing function with multiple fallback strategies
*/
export function showEnhancedError(fieldName, message) {
// Find the field element
const fieldElement = findFieldElement(fieldName);
if (!fieldElement) {
logVerbose(`Cannot find field element for: ${fieldName}`);
return;
}
// Get or create error configuration
let config = enhancedErrorConfigs.get(fieldName);
if (!config) {
config = {
fieldName,
element: fieldElement
};
enhancedErrorConfigs.set(fieldName, config);
}
// Find or create error element
const errorElement = findOrCreateErrorElement(fieldElement, fieldName);
config.errorElement = errorElement;
// Set error message
const errorMessage = message || config.customMessage || 'This field is required';
if (!config.customMessage || message) {
errorElement.textContent = errorMessage;
}
// Apply error styling to the field
addClass(fieldElement, CSS_CLASSES.ERROR_FIELD);
// Show error element with multiple strategies
showErrorElementRobustly(errorElement);
// Add active class for additional styling
addClass(errorElement, CSS_CLASSES.ACTIVE_ERROR);
logVerbose(`Enhanced error shown for field: ${fieldName}`, {
message: errorMessage,
elementVisible: errorElement.offsetParent !== null,
hasActiveClass: errorElement.classList.contains(CSS_CLASSES.ACTIVE_ERROR)
});
}
/**
* Clear enhanced error with multiple strategies
*/
export function clearEnhancedError(fieldName) {
const config = enhancedErrorConfigs.get(fieldName);
if (!config || !config.errorElement) {
logVerbose(`Cannot clear error for field: ${fieldName} - no config or error element`);
return;
}
// Remove error styling from field
removeClass(config.element, CSS_CLASSES.ERROR_FIELD);
// Hide error element
hideErrorElementRobustly(config.errorElement);
// Remove active class
removeClass(config.errorElement, CSS_CLASSES.ACTIVE_ERROR);
// Clear text content
config.errorElement.textContent = '';
logVerbose(`Enhanced error cleared for field: ${fieldName}`);
}
/**
* Show error element using multiple strategies for maximum compatibility
*/
function showErrorElementRobustly(errorElement) {
// Strategy 1: Use setProperty with !important (highest priority)
errorElement.style.setProperty('display', 'block', 'important');
errorElement.style.setProperty('visibility', 'visible', 'important');
errorElement.style.setProperty('opacity', '1', 'important');
// Strategy 2: Apply inline styles as fallback
errorElement.style.display = 'block';
errorElement.style.visibility = 'visible';
errorElement.style.opacity = '1';
// Strategy 3: Remove any hidden classes that might exist
errorElement.classList.remove('hidden', 'w-hidden', 'hide');
// Strategy 4: Ensure basic error styling
if (!errorElement.style.color) {
errorElement.style.color = '#e74c3c';
}
if (!errorElement.style.fontSize) {
errorElement.style.fontSize = '0.875rem';
}
if (!errorElement.style.marginTop) {
errorElement.style.marginTop = '0.25rem';
}
}
/**
* Hide error element using multiple strategies
*/
function hideErrorElementRobustly(errorElement) {
// Strategy 1: Use setProperty with !important
errorElement.style.setProperty('display', 'none', 'important');
errorElement.style.setProperty('visibility', 'hidden', 'important');
errorElement.style.setProperty('opacity', '0', 'important');
// Strategy 2: Apply inline styles as fallback
errorElement.style.display = 'none';
errorElement.style.visibility = 'hidden';
errorElement.style.opacity = '0';
}
/**
* Find field element using multiple strategies
*/
function findFieldElement(fieldName) {
// Strategy 1: Find by name attribute
let element = document.querySelector(`input[name="${fieldName}"], select[name="${fieldName}"], textarea[name="${fieldName}"]`);
// Strategy 2: Find by data-step-field-name
if (!element) {
element = document.querySelector(`[data-step-field-name="${fieldName}"]`);
}
// Strategy 3: Find by ID
if (!element) {
element = document.getElementById(fieldName);
}
// Strategy 4: Find within current visible step
if (!element) {
const visibleStep = document.querySelector('.step_wrapper[style*="flex"]');
if (visibleStep) {
element = visibleStep.querySelector(`input[name="${fieldName}"], select[name="${fieldName}"], textarea[name="${fieldName}"]`);
}
}
return element;
}
/**
* Initialize enhanced error handling for all fields in the form
*/
export function initializeEnhancedErrorHandling() {
logVerbose('Initializing enhanced error handling...');
// Find all input, select, and textarea elements
const formElements = document.querySelectorAll('input, select, textarea');
formElements.forEach((element) => {
const input = element;
const fieldName = input.name || input.getAttribute('data-step-field-name') || input.id;
if (fieldName) {
// Create configuration for this field
const config = {
fieldName,
element: input,
customMessage: getCustomErrorMessage(input) || undefined
};
enhancedErrorConfigs.set(fieldName, config);
// Ensure error element exists
findOrCreateErrorElement(input, fieldName);
}
});
logVerbose(`Enhanced error handling initialized for ${enhancedErrorConfigs.size} fields`);
}
/**
* Get custom error message from element attributes or nearby elements
*/
function getCustomErrorMessage(element) {
// Check for custom message in data attributes
const customMessage = element.getAttribute('data-error-message') ||
element.getAttribute('data-validation-message');
if (customMessage) {
return customMessage;
}
// Check for custom message in nearby error element
const wrapper = element.closest('.form-field_wrapper, .multi-form_input-field');
if (wrapper) {
const errorElement = wrapper.querySelector('.form_error-message');
if (errorElement && errorElement.textContent &&
errorElement.textContent !== 'This is some text inside of a div block.') {
return errorElement.textContent.trim();
}
}
return null;
}
/**
* Clear all enhanced errors
*/
export function clearAllEnhancedErrors() {
enhancedErrorConfigs.forEach((config, fieldName) => {
clearEnhancedError(fieldName);
});
logVerbose('All enhanced errors cleared');
}
/**
* Get statistics about enhanced error handling
*/
export function getEnhancedErrorStats() {
const stats = {
totalFields: enhancedErrorConfigs.size,
fieldsWithCustomMessages: 0,
fieldsWithErrorElements: 0
};
enhancedErrorConfigs.forEach(config => {
if (config.customMessage) {
stats.fieldsWithCustomMessages++;
}
if (config.errorElement) {
stats.fieldsWithErrorElements++;
}
});
return stats;
}
//# sourceMappingURL=enhancedErrors.js.map