UNPKG

form-functionality-library

Version:

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

411 lines 13.2 kB
/** * Utility functions for the form functionality library */ import { DEFAULTS } from '../config.js'; // Simple query cache to improve performance const queryCache = new Map(); /** * Enhanced logging with consistent formatting */ export function logVerbose(message, data) { if (!DEFAULTS.DEBUG) return; const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const prefix = `${DEFAULTS.LOG_PREFIX} [${timestamp}]`; if (data !== undefined) { console.log(`${prefix} ${message}`, data); } else { console.log(`${prefix} ${message}`); } } /** * Clear query cache (useful for dynamic content) */ export function clearQueryCache() { queryCache.clear(); } /** * Query all elements by data attribute with caching */ export function queryAllByAttr(selector, root = document) { // Only cache document-level queries to avoid stale references if (root === document) { const cacheKey = `all:${selector}`; if (queryCache.has(cacheKey)) { return queryCache.get(cacheKey); } const result = root.querySelectorAll(selector); queryCache.set(cacheKey, result); return result; } return root.querySelectorAll(selector); } /** * Query single element by data attribute with caching */ export function queryByAttr(selector, root = document) { // Only cache document-level queries to avoid stale references if (root === document) { const cacheKey = `single:${selector}`; if (queryCache.has(cacheKey)) { return queryCache.get(cacheKey); } const result = root.querySelector(selector); queryCache.set(cacheKey, result); return result; } return root.querySelector(selector); } /** * Get attribute value from element */ export function getAttrValue(element, attribute) { return element.getAttribute(attribute); } /** * Set attribute value on element */ export function setAttrValue(element, attribute, value) { element.setAttribute(attribute, value); } /** * Remove attribute from element */ export function removeAttr(element, attribute) { element.removeAttribute(attribute); } /** * Check if element has attribute */ export function hasAttr(element, attribute) { return element.hasAttribute(attribute); } /** * Add CSS class to element */ export function addClass(element, className) { element.classList.add(className); } /** * Remove CSS class from element */ export function removeClass(element, className) { element.classList.remove(className); } /** * Toggle CSS class on element */ export function toggleClass(element, className, force) { element.classList.toggle(className, force); } /** * Check if element has CSS class */ export function hasClass(element, className) { return element.classList.contains(className); } /** * Show element (remove hidden-step class and restore display) * Enhanced for progressive disclosure reliability */ export function showElement(element) { removeClass(element, 'hidden-step'); // Get the original display value from data attribute or compute it const originalDisplay = element.getAttribute('data-original-display') || ''; // Enhanced display detection for step_wrapper elements const shouldBeFlex = element.classList.contains('flex') || element.classList.contains('d-flex') || element.classList.contains('step_wrapper') || element.classList.contains('step-wrapper') || getComputedStyle(element).display === 'flex'; // Determine the appropriate display value let displayValue = 'block'; if (originalDisplay && originalDisplay !== 'none') { displayValue = originalDisplay; } else if (shouldBeFlex) { displayValue = 'flex'; } // Force visibility with maximum specificity element.style.setProperty('display', displayValue, 'important'); element.style.setProperty('visibility', 'visible', 'important'); element.style.setProperty('opacity', '1', 'important'); // Remove any transform that might hide the element element.style.setProperty('transform', 'none', 'important'); // Ensure no height/width restrictions element.style.removeProperty('height'); element.style.removeProperty('max-height'); element.style.removeProperty('width'); element.style.removeProperty('max-width'); console.log(`🔄 [Utils] Showing element:`, { element: element, tagName: element.tagName, id: element.id, className: element.className, dataAnswer: element.getAttribute('data-answer'), displayValue, originalDisplay: originalDisplay || 'none stored', currentDisplay: element.style.display, computedDisplay: getComputedStyle(element).display, computedVisibility: getComputedStyle(element).visibility, computedOpacity: getComputedStyle(element).opacity, hasHiddenClass: element.classList.contains('hidden-step'), isVisible: isVisible(element) }); } /** * Hide element (add hidden-step class and set display none) */ export function hideElement(element) { // Store the original display value before hiding const computedDisplay = getComputedStyle(element).display; if (computedDisplay && computedDisplay !== 'none') { element.setAttribute('data-original-display', computedDisplay); } addClass(element, 'hidden-step'); element.style.setProperty('display', 'none', 'important'); element.style.setProperty('visibility', 'hidden', 'important'); element.style.setProperty('opacity', '0', 'important'); console.log(`🔄 [Utils] Hiding element:`, { element: element, tagName: element.tagName, id: element.id, className: element.className, originalDisplay: computedDisplay, currentDisplay: element.style.display, computedDisplay: getComputedStyle(element).display, computedVisibility: getComputedStyle(element).visibility, hasHiddenClass: element.classList.contains('hidden-step'), isVisible: isVisible(element) }); } /** * Check if element is visible */ export function isVisible(element) { const style = getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && !hasClass(element, 'hidden-step'); } /** * Debounce function calls */ export function debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; } /** * Get form data as object */ export function getFormData(form) { const formData = new FormData(form); const data = {}; for (const [key, value] of formData.entries()) { if (data[key]) { // Handle multiple values (checkboxes, multi-select) if (Array.isArray(data[key])) { data[key].push(value); } else { data[key] = [data[key], value]; } } else { data[key] = value; } } return data; } /** * Get all form inputs within an element */ export function getFormInputs(element) { return Array.from(element.querySelectorAll('input, select, textarea')); } /** * Check if element is a form input */ export function isFormInput(element) { return element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement; } /** * Get input value safely */ export function getInputValue(input) { if (input instanceof HTMLInputElement) { if (input.type === 'checkbox' || input.type === 'radio') { return input.checked ? input.value : ''; } return input.value; } if (input instanceof HTMLSelectElement && input.multiple) { return Array.from(input.selectedOptions).map(option => option.value); } return input.value; } /** * Set input value safely */ export function setInputValue(input, value) { if (input instanceof HTMLInputElement) { if (input.type === 'checkbox' || input.type === 'radio') { input.checked = Array.isArray(value) ? value.includes(input.value) : value === input.value; } else { input.value = Array.isArray(value) ? value[0] || '' : value; } } else if (input instanceof HTMLSelectElement && input.multiple && Array.isArray(value)) { Array.from(input.options).forEach(option => { option.selected = value.includes(option.value); }); } else { input.value = Array.isArray(value) ? value[0] || '' : value; } } /** * Create delegated event listener */ export function delegateEvent(root, eventType, selector, handler) { const delegatedHandler = (event) => { const target = event.target?.closest(selector); if (target) { handler(event, target); } }; root.addEventListener(eventType, delegatedHandler); // Return cleanup function return () => { root.removeEventListener(eventType, delegatedHandler); }; } /** * Centralized Field Coordinator * Handles all field input events and notifies interested modules via events */ import { FormState } from './formState.js'; import { formEvents } from './events.js'; let fieldCoordinatorInitialized = false; let fieldCoordinatorCleanup = []; /** * Initialize centralized field coordinator * This replaces individual field listeners in branching, validation, and summary modules */ export function initFieldCoordinator(root = document) { if (fieldCoordinatorInitialized) { logVerbose('Field coordinator already initialized, cleaning up first'); resetFieldCoordinator(); } logVerbose('Initializing centralized field coordinator'); // Single event listener for all form field changes const cleanup1 = delegateEvent(root, 'input', 'input, select, textarea', handleFieldInput); const cleanup2 = delegateEvent(root, 'change', 'input, select, textarea', handleFieldChange); const cleanup3 = delegateEvent(root, 'blur', 'input, select, textarea', handleFieldBlur); fieldCoordinatorCleanup.push(cleanup1, cleanup2, cleanup3); fieldCoordinatorInitialized = true; logVerbose('Field coordinator initialization complete'); } /** * Handle field input events (real-time) */ function handleFieldInput(event, target) { if (!isFormInput(target)) return; const fieldName = getFieldName(target); const fieldValue = getInputValue(target); if (fieldName) { // Store in FormState (single source of truth) FormState.setField(fieldName, fieldValue); logVerbose('Field input detected', { fieldName, value: fieldValue, eventType: 'input' }); // Notify interested modules via events formEvents.emit('field:input', { fieldName, value: fieldValue, element: target, eventType: 'input' }); } } /** * Handle field change events (when value finalizes) */ function handleFieldChange(event, target) { if (!isFormInput(target)) return; const fieldName = getFieldName(target); const fieldValue = getInputValue(target); if (fieldName) { // Store in FormState (single source of truth) FormState.setField(fieldName, fieldValue); logVerbose('Field change detected', { fieldName, value: fieldValue, eventType: 'change' }); // Notify interested modules via events formEvents.emit('field:change', { fieldName, value: fieldValue, element: target, eventType: 'change' }); } } /** * Handle field blur events (for validation) */ function handleFieldBlur(event, target) { if (!isFormInput(target)) return; const fieldName = getFieldName(target); const fieldValue = getInputValue(target); if (fieldName) { logVerbose('Field blur detected', { fieldName, value: fieldValue, eventType: 'blur' }); // Notify interested modules via events formEvents.emit('field:blur', { fieldName, value: fieldValue, element: target, eventType: 'blur' }); } } /** * Get field name from element */ function getFieldName(element) { const htmlElement = element; return htmlElement.name || getAttrValue(element, 'data-step-field-name') || null; } /** * Reset field coordinator */ export function resetFieldCoordinator() { if (!fieldCoordinatorInitialized) { logVerbose('Field coordinator not initialized, nothing to reset'); return; } logVerbose('Resetting field coordinator'); fieldCoordinatorCleanup.forEach(cleanup => cleanup()); fieldCoordinatorCleanup = []; fieldCoordinatorInitialized = false; logVerbose('Field coordinator reset complete'); } //# sourceMappingURL=utils.js.map