UNPKG

form-functionality-library

Version:

A modular, flexible form functionality library for Webflow forms supporting single-step, multi-step, and branching forms

366 lines 14.9 kB
/** * Error handling and display module - Legacy System * Note: v1.9.0 introduces webflowNativeErrors.ts for Webflow-native error handling */ import { logVerbose, addClass, removeClass } from './utils.js'; import { CSS_CLASSES, SELECTORS } from '../config.js'; import { getAttrValue } from './utils.js'; import { formEvents } from './events.js'; let errorConfigs = new Map(); let errorStates = new Map(); let cssInjected = false; // V1.7.0 WEBFLOW HARMONY: Removed nuclear CSS approach // Now following Webflow's official patterns for elegant integration /** * Inject required CSS for error visibility - DISABLED FOR TESTING */ function injectErrorCSS() { // TEMPORARILY DISABLED - Let's see what Webflow does natively logVerbose('📦 [Errors] CSS injection disabled - testing Webflow native behavior'); cssInjected = true; return; // CSS injection disabled for testing Webflow native behavior } /** * Initialize error handling */ export function initErrors(root = document) { logVerbose('Initializing error handling'); // Inject required CSS for error visibility injectErrorCSS(); // Find all form inputs and set up error configurations const inputs = root.querySelectorAll('input, select, textarea'); inputs.forEach(input => { const fieldName = input.name || getAttrValue(input, 'data-step-field-name'); if (fieldName) { errorConfigs.set(fieldName, { fieldName, element: input, customMessage: getAttrValue(input, 'data-error-message') || undefined }); } }); formEvents.registerModule('errors'); logVerbose(`Error handling initialized for ${errorConfigs.size} fields`); } /** * Scroll to field if it's not in viewport */ function scrollToFieldIfNeeded(element) { const rect = element.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); if (!isVisible) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); logVerbose(`Scrolled to field with error: ${element.name || 'unnamed'}`); } } /** * Show error for a specific field */ export function showError(fieldName, message) { const config = errorConfigs.get(fieldName); if (!config) { logVerbose(`Cannot show error for unknown field: ${fieldName}`); return; } // ENHANCED: Prioritize custom message from HTML, then passed message, then fallback const errorMessage = config.customMessage || message || 'This field has an error'; logVerbose(`Showing error for field: ${fieldName}`, { message: errorMessage, hasCustomMessage: !!config.customMessage, messageSource: config.customMessage ? 'html' : message ? 'parameter' : 'fallback' }); // Add error styling to the field addClass(config.element, CSS_CLASSES.ERROR_FIELD); // Create or update error message element const errorElement = findOrCreateErrorElement(config); if (errorElement) { // ENHANCED: Only update text if we don't have custom HTML content or if it's a validation message if (!config.customMessage || message) { errorElement.textContent = errorMessage; } // V1.8.0 CLEAN FIX: Find error element within form field wrapper const visibleStep = document.querySelector('.step_wrapper[style*="flex"]'); const fieldInput = visibleStep?.querySelector(`input[name="${config.element.getAttribute('name')}"]`) || config.element; // Find error element by traversing up to .form-field_wrapper and searching within it const formFieldWrapper = fieldInput.closest('.form-field_wrapper'); const currentStepErrorElement = formFieldWrapper?.querySelector('.form_error-message[data-form="required"]'); const actualErrorElement = (currentStepErrorElement || errorElement); // V1.8.3 WEBFLOW HARMONY: Work with Webflow's natural flow // Simple, elegant approach that follows Webflow's patterns // Strategy 1: Use Webflow's standard display approach actualErrorElement.style.display = 'block'; // Strategy 2: Add Webflow-compatible error styling actualErrorElement.style.color = '#e74c3c'; actualErrorElement.style.fontSize = '0.875rem'; actualErrorElement.style.marginTop = '0.25rem'; actualErrorElement.style.lineHeight = '1.4'; // Strategy 3: Remove any hiding attributes that might exist actualErrorElement.removeAttribute('hidden'); // Strategy 4: Add accessibility and visual state actualErrorElement.setAttribute('aria-live', 'polite'); actualErrorElement.setAttribute('role', 'alert'); // Still add the class for any additional styling addClass(actualErrorElement, CSS_CLASSES.ACTIVE_ERROR); config.errorElement = actualErrorElement; logVerbose(`Error element activated for field: ${fieldName}`, { elementVisible: actualErrorElement.offsetParent !== null, hasActiveClass: actualErrorElement.classList.contains(CSS_CLASSES.ACTIVE_ERROR), hasInlineStyles: true, usingCurrentStepElement: !!currentStepErrorElement, elementLookupSuccess: currentStepErrorElement ? 'found in current step' : 'using cached element' }); } // Scroll to field if it's not visible scrollToFieldIfNeeded(config.element); } /** * Clear error for a specific field */ export function clearError(fieldName) { const config = errorConfigs.get(fieldName); if (!config) { logVerbose(`Cannot clear error for unknown field: ${fieldName}`); return; } logVerbose(`Clearing error for field: ${fieldName}`); // Remove error styling from the field removeClass(config.element, CSS_CLASSES.ERROR_FIELD); // Hide error message element if (config.errorElement) { config.errorElement.textContent = ''; // V1.7.0 WEBFLOW HARMONY: Simple, clean error clearing // Use !important to ensure proper hiding config.errorElement.style.setProperty('display', 'none', 'important'); removeClass(config.errorElement, CSS_CLASSES.ACTIVE_ERROR); } } /** * Clear all errors */ export function clearAllErrors() { logVerbose('Clearing all field errors'); errorConfigs.forEach((config, fieldName) => { clearError(fieldName); }); } /** * Show multiple errors at once */ export function showErrors(errors) { logVerbose('Showing multiple errors', errors); Object.entries(errors).forEach(([fieldName, message]) => { showError(fieldName, message); }); } /** * Check if a field has an error */ export function hasError(fieldName) { const config = errorConfigs.get(fieldName); if (!config) return false; return config.element.classList.contains(CSS_CLASSES.ERROR_FIELD); } /** * Get all fields with errors */ export function getFieldsWithErrors() { const fieldsWithErrors = []; errorConfigs.forEach((config, fieldName) => { if (hasError(fieldName)) { fieldsWithErrors.push(fieldName); } }); return fieldsWithErrors; } /** * Set custom error message for a field */ export function setCustomErrorMessage(fieldName, message) { const config = errorConfigs.get(fieldName); if (!config) { logVerbose(`Cannot set custom error message for unknown field: ${fieldName}`); return; } config.customMessage = message; logVerbose(`Custom error message set for field: ${fieldName}`, { message }); } /** * Find or create error message element for a field * ENHANCED: Automatically detects existing .form_error-message elements and uses their custom text */ function findOrCreateErrorElement(config) { // Defensive checks if (!config || !config.element) { logVerbose('Cannot create error element - no config or element provided'); return null; } // Check if element has a parent element if (!config.element.parentElement) { logVerbose(`Cannot create error element for field: ${config.fieldName} - no parent element`, { element: config.element, parentElement: config.element.parentElement, nodeName: config.element.nodeName, id: config.element.id }); return null; } // Look for existing error element in form-field_wrapper structure let errorElement = null; // ENHANCED: First, try to find existing .form_error-message in field wrapper const fieldWrapper = config.element.closest('.form-field_wrapper'); if (fieldWrapper) { // PRIORITY 1: Look for data-form="required" elements first (new standardized approach) errorElement = fieldWrapper.querySelector('[data-form="required"]'); // If found, extract custom message text and store it if (errorElement && errorElement.textContent && errorElement.textContent.trim() !== '') { const customText = errorElement.textContent.trim(); config.customMessage = customText; logVerbose(`Found custom required error message for field: ${config.fieldName}`, { customText }); } // PRIORITY 2: Look for .form_error-message (legacy support) if (!errorElement) { errorElement = fieldWrapper.querySelector('.form_error-message'); // If found, extract custom message text and store it if (errorElement && errorElement.textContent && errorElement.textContent.trim() !== '') { const customText = errorElement.textContent.trim(); config.customMessage = customText; logVerbose(`Found custom error message for field: ${config.fieldName}`, { customText }); } } // Fallback: Look for data-form="error" attribute if (!errorElement) { errorElement = fieldWrapper.querySelector('[data-form="error"]'); } } // ENHANCED: If no wrapper, look for error elements near the input if (!errorElement) { const parentElement = config.element.parentElement; // PRIORITY 1: Look for data-form="required" elements (new standardized approach) errorElement = parentElement.querySelector('[data-form="required"]'); // If found, extract custom message text and store it if (errorElement && errorElement.textContent && errorElement.textContent.trim() !== '') { const customText = errorElement.textContent.trim(); config.customMessage = customText; logVerbose(`Found custom required error message for field: ${config.fieldName}`, { customText }); } // PRIORITY 2: Try to find .form_error-message as sibling or in parent (legacy support) if (!errorElement) { errorElement = parentElement.querySelector('.form_error-message'); // Extract custom message if found if (errorElement && errorElement.textContent && errorElement.textContent.trim() !== '') { const customText = errorElement.textContent.trim(); config.customMessage = customText; logVerbose(`Found custom error message for field: ${config.fieldName}`, { customText }); } } } // Fallback: Look for existing error element by field name (legacy support) if (!errorElement) { try { errorElement = config.element.parentElement.querySelector(`${SELECTORS.ERROR_DISPLAY}[data-field="${config.fieldName}"]`); } catch (error) { logVerbose(`Error finding existing error element for field: ${config.fieldName}`, error); } } if (!errorElement) { try { // Create new error element with proper structure errorElement = document.createElement('div'); errorElement.setAttribute('data-form', 'error'); errorElement.setAttribute('data-field', config.fieldName); errorElement.className = 'form_error-message'; // Insert in form-field_wrapper if available, otherwise fallback to old method if (fieldWrapper) { fieldWrapper.appendChild(errorElement); } else { config.element.parentElement.insertBefore(errorElement, config.element.nextSibling); } logVerbose(`Created new error element for field: ${config.fieldName}`); } catch (error) { logVerbose(`Failed to create error element for field: ${config.fieldName}`, error); return null; } } else { logVerbose(`Found existing error element for field: ${config.fieldName}`, { className: errorElement.className, hasCustomMessage: !!config.customMessage }); } return errorElement; } /** * Highlight field with error (alternative to standard error styling) */ export function highlightFieldError(fieldName, highlightClass = 'field-highlight') { const config = errorConfigs.get(fieldName); if (!config) return; addClass(config.element, highlightClass); // Remove highlight after a delay setTimeout(() => { removeClass(config.element, highlightClass); }, 3000); logVerbose(`Highlighted field with error: ${fieldName}`); } /** * Focus on first field with error */ export function focusFirstError() { const fieldsWithErrors = getFieldsWithErrors(); if (fieldsWithErrors.length > 0) { const firstErrorField = fieldsWithErrors[0]; const config = errorConfigs.get(firstErrorField); if (config && config.element instanceof HTMLInputElement) { config.element.focus(); logVerbose(`Focused on first error field: ${firstErrorField}`); } } } /** * Get error statistics */ export function getErrorStats() { const fieldsWithErrors = getFieldsWithErrors(); return { totalErrors: fieldsWithErrors.length, fieldsWithErrors, hasErrors: fieldsWithErrors.length > 0 }; } /** * Reset error handling */ export function resetErrors() { logVerbose('Resetting error handling'); // Clear all errors clearAllErrors(); // Clear configurations errorConfigs.clear(); logVerbose('Error handling reset complete'); } /** * Get current error state for debugging */ export function getErrorState() { const state = {}; errorStates.forEach((value, key) => { state[key] = { message: value.message }; }); return state; } //# sourceMappingURL=errors.js.map