/** * Frontend JavaScript for Dynamic Multistep Forms */ class DMFForm { constructor() { this.formContainer = document.querySelector('.dmf-form-container'); if (!this.formContainer) return; this.formData = this.getFormData(); this.selectedValues = {}; this.sessionId = this.getSessionId(); this.currentProgressIndex = 0; // Track which progress indicator is currently active this.init(); } init() { this.bindEvents(); this.restoreProgress(); this.checkTextOnlyQuestion(); this.trackPageView(); this.startTimeTracking(); this.bindAbandonmentTracking(); } getFormData() { const dataScript = document.getElementById('dmf-form-data'); return dataScript ? JSON.parse(dataScript.textContent) : {}; } getSessionId() { let sessionId = localStorage.getItem('dmf_session_id'); if (!sessionId) { sessionId = 'dmf_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('dmf_session_id', sessionId); } return sessionId; } bindEvents() { // Option selection events this.bindOptionEvents(); // Navigation button events this.bindNavigationEvents(); // Input events for text/email/number fields this.bindInputEvents(); } bindOptionEvents() { const options = document.querySelectorAll('.dmf-option'); options.forEach(option => { option.addEventListener('click', (e) => { e.preventDefault(); // Get the actual .dmf-option element even if click was on child const optionElement = e.target.closest('.dmf-option'); if (optionElement) { this.handleOptionClick(optionElement); } }); // Also bind to child elements to ensure clicks work const children = option.querySelectorAll('*'); children.forEach(child => { child.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.handleOptionClick(option); }); }); }); } bindNavigationEvents() { const nextBtn = document.querySelector('.dmf-next-btn'); const prevBtn = document.querySelector('.dmf-prev-btn'); if (nextBtn) { nextBtn.addEventListener('click', (e) => { e.preventDefault(); this.handleNextStep(); }); } if (prevBtn) { prevBtn.addEventListener('click', (e) => { e.preventDefault(); this.handlePrevStep(); }); } } bindInputEvents() { const inputs = document.querySelectorAll('.dmf-input'); inputs.forEach(input => { input.addEventListener('input', (e) => { this.handleInputChange(input); }); input.addEventListener('blur', (e) => { this.validateInput(input); }); }); } handleOptionClick(option) { // IMMEDIATE ALERT FOR DEBUGGING const container = option.closest('.dmf-options'); const variable = container ? container.dataset.variable : 'unknown'; const value = option.dataset.value || 'unknown'; const step = this.formData.current_step || 'unknown'; // Debug removed const optionsContainer = option.closest('.dmf-options'); const variableName = optionsContainer.dataset.variable; const optionValue = option.dataset.value; const isMultipleChoice = optionsContainer.classList.contains('dmf-multiple-choice'); if (isMultipleChoice) { // Handle multiple choice if (option.classList.contains('selected')) { option.classList.remove('selected'); this.removeValueFromArray(variableName, optionValue); } else { option.classList.add('selected'); this.addValueToArray(variableName, optionValue); } } else { // Handle single choice optionsContainer.querySelectorAll('.dmf-option').forEach(opt => { opt.classList.remove('selected'); }); option.classList.add('selected'); this.selectedValues[variableName] = optionValue; } this.updateNextButton(); this.saveProgress(); // Track option selection this.trackEvent('option_selected', { step: this.formData.current_step, variable: variableName, value: optionValue, selection_type: isMultipleChoice ? 'multiple' : 'single', time_to_select: Math.round((Date.now() - this.stepStartTime) / 1000) }); } handleInputChange(input) { const variableName = input.dataset.variable; const value = input.value.trim(); this.selectedValues[variableName] = value; this.updateNextButton(); this.saveProgress(); } addValueToArray(variableName, value) { if (!this.selectedValues[variableName]) { this.selectedValues[variableName] = []; } if (!this.selectedValues[variableName].includes(value)) { this.selectedValues[variableName].push(value); } } removeValueFromArray(variableName, value) { if (this.selectedValues[variableName]) { this.selectedValues[variableName] = this.selectedValues[variableName].filter(v => v !== value); if (this.selectedValues[variableName].length === 0) { delete this.selectedValues[variableName]; } } } updateNextButton() { const nextBtn = document.querySelector('.dmf-next-btn'); const currentVariable = this.formData.variable_name; if (!nextBtn) return; const hasSelection = this.selectedValues[currentVariable] && (Array.isArray(this.selectedValues[currentVariable]) ? this.selectedValues[currentVariable].length > 0 : this.selectedValues[currentVariable] !== ''); if (hasSelection) { nextBtn.disabled = false; nextBtn.classList.add('enabled'); } else { nextBtn.disabled = true; nextBtn.classList.remove('enabled'); } } handleNextStep() { const nextBtn = document.querySelector('.dmf-next-btn'); const currentVariable = this.formData.variable_name; console.log('DMF handleNextStep: formData =', this.formData); console.log('DMF handleNextStep: currentVariable =', currentVariable); console.log('DMF handleNextStep: selectedValues =', this.selectedValues); if (nextBtn.disabled) { this.showError(dmf_ajax.strings.select_option); return; } const responseValue = this.selectedValues[currentVariable]; if (!responseValue || (Array.isArray(responseValue) && responseValue.length === 0)) { this.showError(dmf_ajax.strings.select_option); return; } console.log('DMF handleNextStep: Sending step', this.formData.current_step, 'variable', currentVariable, 'value', responseValue); this.showLoading(); // Send AJAX request to process step fetch(dmf_ajax.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'dmf_process_step', nonce: dmf_ajax.nonce, form_id: this.formData.form_id, current_step: this.formData.current_step, variable_name: currentVariable, response_value: Array.isArray(responseValue) ? responseValue.join(',') : responseValue, session_id: this.sessionId }) }) .then(response => { // Check if response was redirected (indicates product redirect) if (response.redirected) { // If redirected, follow the redirect window.location.href = response.url; return; } return response.json(); }) .then(data => { // If data is undefined, it means we were redirected if (!data) { return; } this.hideLoading(); if (data.success) { // Check if this is a product redirect if (data.data.type === 'product_redirect') { console.log('DMF: Product redirect data:', data.data); console.log('DMF: Product URL:', data.data.product_url); console.log('DMF: Product Name:', data.data.product_name); this.formCompleted = true; this.trackEvent('product_redirect', { step: this.formData.current_step, variable: currentVariable, value: responseValue, product_name: data.data.product_name, total_time: Math.round((Date.now() - this.formStartTime) / 1000) }); // Show message and redirect to product this.showProductRedirect(data.data.product_name, data.data.product_url); return; } // Debug removed - log to console only if (data.data.debug_info) { console.log('DMF DEBUG INFO:', data.data.debug_info); // Show alert with rule details when rule is found if (data.data.debug_info.logic_rule_found && data.data.debug_info.logic_rule_details) { var rule = data.data.debug_info.logic_rule_details; var alertText = "RULE FOUND - DEBUG INFO:\n\n"; alertText += "Rule ID: " + rule.id + "\n"; alertText += "Action Type: " + rule.action_type + "\n"; alertText += "From Step: " + rule.from_step + "\n"; alertText += "From Variable: " + rule.from_variable + "\n"; alertText += "From Value: " + rule.from_value + "\n"; alertText += "To Step: " + rule.to_step + "\n"; alertText += "Action Data: " + (rule.action_data || 'None') + "\n\n"; alertText += "Next Step Result: " + JSON.stringify(data.data.debug_next_step_result) + "\n"; alertText += "Context: " + data.data.debug_context + "\n"; console.log('Debug:', alertText); } } if (data.data.completed) { // Form completed - redirect to completion page console.log('DMF: Form completed, redirecting to completion page'); this.formCompleted = true; this.trackEvent('form_completed', { total_time: Math.round((Date.now() - this.formStartTime) / 1000), total_steps: this.formData.total_steps, completion_timestamp: new Date().toISOString() }); window.location.href = data.data.completion_url; } else { // Go to next step via AJAX (no page reload) console.log('DMF: Going to next step:', data.data.next_step); this.trackEvent('step_completed', { step: this.formData.current_step, variable: currentVariable, value: responseValue, time_on_step: Math.round((Date.now() - this.stepStartTime) / 1000), total_time_so_far: Math.round((Date.now() - this.formStartTime) / 1000) }); // Update progress bar (move forward) this.updateProgressBar('next'); // Load next step via AJAX instead of full page reload // Pass the step number and responses URL part this.loadStepAjax(data.data.next_step, data.data.responses_url_part || ''); } } else { this.showError(data.data || dmf_ajax.strings.error); } }) .catch(error => { this.hideLoading(); console.error('Error:', error); this.showError(dmf_ajax.strings.error); }); } handlePrevStep() { const prevBtn = document.querySelector('.dmf-prev-btn'); if (!prevBtn || prevBtn.disabled) return; const prevStep = parseInt(prevBtn.dataset.step); this.trackEvent('step_back', { from_step: this.formData.current_step, to_step: prevStep, time_on_step: Math.round((Date.now() - this.stepStartTime) / 1000) }); // Update progress bar (move backward) this.updateProgressBar('prev'); // Load previous step via AJAX, passing current step to remove it from sequence this.loadStepAjax(prevStep, '', true, this.formData.current_step); } loadStepAjax(stepNumber, responsesUrlPart, isGoingBack = false, currentStepToRemove = null) { this.showLoading(); const params = { action: 'dmf_get_step_html', nonce: dmf_ajax.nonce, form_id: this.formData.form_id, step: stepNumber }; // If going back, send the current step to remove from sequence if (isGoingBack && currentStepToRemove) { params.going_back = '1'; params.remove_step = currentStepToRemove; } // Fetch the step content via AJAX fetch(dmf_ajax.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams(params) }) .then(response => response.json()) .then(data => { this.hideLoading(); if (data.success) { console.log('DMF AJAX: Loaded step', stepNumber, 'with responses:', responsesUrlPart); // Update the form content const formContainer = document.querySelector('.dmf-form-content'); if (formContainer) { formContainer.innerHTML = data.data.html; } // Re-read form data from the new injected script tag const formDataScript = document.getElementById('dmf-form-data'); if (formDataScript) { try { const newFormData = JSON.parse(formDataScript.textContent); console.log('DMF AJAX: New form data:', newFormData); // Update form data with new step info this.formData = Object.assign({}, this.formData, newFormData); // If there's a saved response, restore it in selectedValues if (newFormData.saved_response) { const variableName = newFormData.variable_name; const savedValue = newFormData.saved_response; // Handle multiple choice (comma-separated values) if (savedValue.indexOf(',') !== -1) { this.selectedValues[variableName] = savedValue.split(','); } else { this.selectedValues[variableName] = savedValue; } console.log('DMF AJAX: Restored saved response:', variableName, '=', this.selectedValues[variableName]); } } catch (e) { console.error('DMF AJAX: Error parsing form data:', e); } } // Update URL without page reload - use query parameter format for Google Analytics const currentPath = window.location.pathname; const pathParts = currentPath.split('/').filter(p => p); // Find the form slug (first part) const formSlug = pathParts[0]; // Build new URL: /form-slug/step/number?r1=value1&r2=value2&r3=value3 let newUrl = '/' + formSlug + '/step/' + stepNumber; if (responsesUrlPart && responsesUrlPart.trim() !== '') { // responsesUrlPart already contains the query parameters (r1=value1&r2=value2...) newUrl += '?' + responsesUrlPart; } console.log('DMF AJAX: Updating URL to:', newUrl); // Use replaceState instead of pushState to prevent browser back navigation history.replaceState({step: stepNumber}, '', newUrl); // Update internal state this.stepStartTime = Date.now(); // Re-initialize form handlers for the new content this.init(); } else { this.showError(data.data || 'Error loading step'); } }) .catch(error => { this.hideLoading(); console.error('Error loading step:', error); this.showError('Error loading step'); }); } validateInput(input) { const value = input.value.trim(); const inputType = input.type; let isValid = true; let errorMessage = ''; if (input.hasAttribute('required') && !value) { isValid = false; errorMessage = 'This field is required.'; } else if (inputType === 'email' && value && !this.isValidEmail(value)) { isValid = false; errorMessage = 'Please enter a valid email address.'; } else if (inputType === 'number' && value && isNaN(value)) { isValid = false; errorMessage = 'Please enter a valid number.'; } if (isValid) { input.classList.remove('error'); this.clearInputError(input); } else { input.classList.add('error'); this.showInputError(input, errorMessage); } return isValid; } isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } showInputError(input, message) { let errorElement = input.parentNode.querySelector('.dmf-input-error'); if (!errorElement) { errorElement = document.createElement('div'); errorElement.className = 'dmf-input-error'; input.parentNode.appendChild(errorElement); } errorElement.textContent = message; } clearInputError(input) { const errorElement = input.parentNode.querySelector('.dmf-input-error'); if (errorElement) { errorElement.remove(); } } saveProgress() { if (!this.formData.settings.save_progress) return; const progressData = { form_id: this.formData.form_id, current_step: this.formData.current_step, selected_values: this.selectedValues, timestamp: Date.now() }; localStorage.setItem('dmf_progress_' + this.formData.form_id, JSON.stringify(progressData)); } restoreProgress() { if (!this.formData.settings.save_progress) return; const savedProgress = localStorage.getItem('dmf_progress_' + this.formData.form_id); if (!savedProgress) return; const progressData = JSON.parse(savedProgress); // Only restore if it's for the current step if (progressData.current_step !== this.formData.current_step) return; this.selectedValues = progressData.selected_values || {}; // Restore UI state this.restoreUIState(); } restoreUIState() { const currentVariable = this.formData.variable_name; const savedValue = this.selectedValues[currentVariable]; if (!savedValue) return; // Restore option selections if (Array.isArray(savedValue)) { // Multiple choice savedValue.forEach(value => { const option = document.querySelector(`[data-value="${value}"]`); if (option) { option.classList.add('selected'); } }); } else { // Single choice or input const option = document.querySelector(`[data-value="${savedValue}"]`); if (option) { option.classList.add('selected'); } else { // Check for input field const input = document.querySelector(`[data-variable="${currentVariable}"]`); if (input) { input.value = savedValue; } } } this.updateNextButton(); } showLoading() { const nextBtn = document.querySelector('.dmf-next-btn'); if (nextBtn) { nextBtn.disabled = true; nextBtn.textContent = dmf_ajax.strings.loading; nextBtn.classList.add('loading'); } } hideLoading() { const nextBtn = document.querySelector('.dmf-next-btn'); if (nextBtn) { nextBtn.classList.remove('loading'); const isLastStep = this.formData.current_step === this.formData.total_steps; nextBtn.textContent = isLastStep ? 'Completar' : 'Siguiente'; this.updateNextButton(); } } showError(message) { // Remove existing error messages const existingError = document.querySelector('.dmf-error-message'); if (existingError) { existingError.remove(); } // Create and show new error message const errorDiv = document.createElement('div'); errorDiv.className = 'dmf-error-message'; errorDiv.textContent = message; const formContent = document.querySelector('.dmf-form-content'); if (formContent) { formContent.insertBefore(errorDiv, formContent.firstChild); } // Auto-hide after 5 seconds setTimeout(() => { if (errorDiv.parentNode) { errorDiv.remove(); } }, 5000); } trackPageView() { this.trackEvent('step_viewed', { step: this.formData.current_step, form_slug: this.formData.form_slug }); } updateProgressBar(direction) { const stepIndicators = document.querySelectorAll('.dmf-progress-step'); if (stepIndicators.length === 0) return; if (direction === 'next') { // Move to next indicator (paint next line) if (this.currentProgressIndex < stepIndicators.length - 1) { this.currentProgressIndex++; } } else if (direction === 'prev') { // Move to previous indicator (unpaint last line) if (this.currentProgressIndex > 0) { this.currentProgressIndex--; } } // Update all indicators based on currentProgressIndex stepIndicators.forEach((step, index) => { if (index < this.currentProgressIndex) { // Completed step.classList.add('completed'); step.classList.remove('active'); } else if (index === this.currentProgressIndex) { // Current/Active step.classList.add('active'); step.classList.remove('completed'); } else { // Future step.classList.remove('active', 'completed'); } }); console.log('DMF: Progress bar - index', this.currentProgressIndex, 'of', stepIndicators.length - 1); } trackEvent(eventType, eventData = {}) { // Track with Google Analytics if available if (typeof gtag !== 'undefined') { gtag('event', eventType, { 'custom_map': { form_id: this.formData.form_id, form_slug: this.formData.form_slug, session_id: this.sessionId, ...eventData } }); } // Track with internal analytics fetch(dmf_ajax.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'dmf_track_event', nonce: dmf_ajax.nonce, form_id: this.formData.form_id, event_type: eventType, event_data: JSON.stringify({ session_id: this.sessionId, ...eventData }) }) }).catch(error => { console.log('Analytics tracking error:', error); }); } startTimeTracking() { this.stepStartTime = Date.now(); this.formStartTime = this.formStartTime || Date.now(); } bindAbandonmentTracking() { // Track when user leaves the page window.addEventListener('beforeunload', () => { // Only track abandonment if form not completed if (!this.formCompleted) { this.trackEvent('form_abandoned', { step: this.formData.current_step, time_spent: Math.round((Date.now() - this.formStartTime) / 1000), last_step: this.formData.current_step, progress_percentage: Math.round((this.formData.current_step / this.formData.total_steps) * 100), abandonment_point: this.formData.current_step, selected_values: Object.keys(this.selectedValues).length }); } }); // Track inactivity (user idle for more than 5 minutes) let idleTimer; const resetIdleTimer = () => { clearTimeout(idleTimer); idleTimer = setTimeout(() => { if (!this.formCompleted) { this.trackEvent('user_idle', { step: this.formData.current_step, idle_duration: 300 // 5 minutes }); } }, 300000); // 5 minutes }; ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'].forEach(event => { document.addEventListener(event, resetIdleTimer, true); }); resetIdleTimer(); } // Check if current question is text_only and auto-select it checkTextOnlyQuestion() { const textOnlyContent = document.querySelector('.dmf-text-only-content'); if (textOnlyContent) { const hiddenInput = textOnlyContent.querySelector('input[type="hidden"]'); if (hiddenInput) { const variableName = hiddenInput.dataset.variable; const value = hiddenInput.value; // Auto-select the text-only question this.selectedValues[variableName] = value; this.updateNextButton(); this.saveProgress(); // Track that this step was auto-selected this.trackEvent('text_only_auto_selected', { step: this.formData.current_step, variable: variableName, value: value }); } } } // Handle form abandonment tracking trackAbandonment() { this.trackEvent('form_abandoned', { last_step: this.formData.current_step, selected_values: this.selectedValues, time_on_step: Date.now() - this.stepStartTime }); } // Show product redirect message and redirect showProductRedirect(productName, productUrl) { // Replace current history entry with form start URL // So when user clicks back from product page, they return to /empezar const formSlug = this.formData.form_slug || 'empezar'; const formStartUrl = '/' + formSlug; history.replaceState({}, '', formStartUrl); // Redirect immediately without modal window.location.href = productUrl; } } // AJAX form navigation for single-page experience class DMFAjaxNavigation { constructor() { this.init(); } init() { // Handle browser back/forward window.addEventListener('popstate', (e) => { if (e.state && e.state.dmfStep) { this.loadStep(e.state.formId, e.state.dmfStep, false); } }); // Track initial page state const formData = this.getFormData(); if (formData.form_id) { const initialState = { formId: formData.form_id, dmfStep: formData.current_step }; history.replaceState(initialState, '', window.location.href); } } getFormData() { const dataScript = document.getElementById('dmf-form-data'); return dataScript ? JSON.parse(dataScript.textContent) : {}; } loadStep(formId, step, updateHistory = true) { fetch(dmf_ajax.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'dmf_get_step', nonce: dmf_ajax.nonce, form_id: formId, step: step }) }) .then(response => response.json()) .then(data => { if (data.success) { // Update page content const formContainer = document.querySelector('.dmf-form-container'); if (formContainer) { formContainer.innerHTML = data.data.content; // Reinitialize form new DMFForm(); // Update URL if needed if (updateHistory) { const formData = this.getFormData(); const newUrl = dmf_ajax.home_url + '/form/' + formData.form_slug + '/step/' + step; const newState = { formId: formId, dmfStep: step }; history.pushState(newState, '', newUrl); } } } }) .catch(error => { console.error('Error loading step:', error); }); } } // Form validation utilities class DMFValidation { static validateStep(formData, selectedValues) { const currentVariable = formData.variable_name; const questionType = formData.question_type; const value = selectedValues[currentVariable]; const errors = []; // Check if required field has value if (!value || (Array.isArray(value) && value.length === 0)) { errors.push('This field is required.'); return errors; } // Type-specific validation switch (questionType) { case 'email': if (!this.isValidEmail(value)) { errors.push('Please enter a valid email address.'); } break; case 'number': if (isNaN(value)) { errors.push('Please enter a valid number.'); } break; } return errors; } static isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Initialize main form functionality if (document.querySelector('.dmf-form-container')) { window.dmfForm = new DMFForm(); window.dmfAjaxNav = new DMFAjaxNavigation(); } // Handle form abandonment tracking let formAbandoned = false; window.addEventListener('beforeunload', function() { if (window.dmfForm && !formAbandoned) { window.dmfForm.trackAbandonment(); formAbandoned = true; } }); // Track time spent on step if (window.dmfForm) { window.dmfForm.stepStartTime = Date.now(); } }); // Utility functions for external use window.DMFUtils = { // Get current form data getFormData: function() { return window.dmfForm ? window.dmfForm.formData : null; }, // Get selected values getSelectedValues: function() { return window.dmfForm ? window.dmfForm.selectedValues : {}; }, // Programmatically select option selectOption: function(variableName, value) { if (window.dmfForm) { window.dmfForm.selectedValues[variableName] = value; window.dmfForm.restoreUIState(); window.dmfForm.updateNextButton(); } }, // Go to specific step gotoStep: function(step) { if (window.dmfAjaxNav && window.dmfForm) { window.dmfAjaxNav.loadStep(window.dmfForm.formData.form_id, step); } }, // Track custom event trackEvent: function(eventType, eventData) { if (window.dmfForm) { window.dmfForm.trackEvent(eventType, eventData); } } };