PageSourceSearch

https://www.smilebrilliant.com/wp-content/plugins/sb-wizard/assets…/conditional-logic.js?ver=2.0.0.1790308175

js smilebrilliant.com collected 2026-09-25 03:49:56 UTC 50,098 bytes, 1,343 lines download raw bytes

1/**
2 * SB Wizard Conditional Logic Frontend Engine
3 * 
4 * Handles dynamic show/hide, navigation state, and form validation
5 * with perfect Next/Previous button management
6 */
7
8class SBWizardConditionalLogic {
9    constructor(config = {}) {
10        this.config = {
11            formSelector: '.sb-wizard-form',
12            questionSelector: '.sb-question',
13            nextButtonSelector: '.sb-wizard-btn-next', 
14            prevButtonSelector: '.sb-wizard-btn-prev',
15            submitButtonSelector: '.sb-wizard-btn-submit',
16            progressSelector: '.sb-wizard-progress',
17            autoSaveEnabled: false, // Disable auto-save to prevent AJAX errors
18            autoSaveDelay: 1000,
19            animationDuration: 300,
20            debugMode: false,
21            ...config
22        };
23        
24        // Core state management
25        this.questions = new Map();
26        this.conditionalRules = new Map();
27        this.currentAnswers = new Map();
28        this.visibilityState = new Map();
29        this.navigationHistory = [];
30        this.currentStep = 0;
31        this.totalSteps = 0;
32        
33        // Navigation state tracking
34        this.canGoNext = false;
35        this.canGoPrev = false;
36        this.visibleQuestions = [];
37        this.requiredQuestions = [];
38        
39        // Performance optimization
40        this.debounceTimers = new Map();
41        this.cachedSelectors = new Map();
42        
43        this.init();
44    }
45    
46    /**
47     * Initialize the conditional logic system
48     */
49    init() {
50        this.log('Initializing SB Wizard Conditional Logic');
51        
52        // Load questions and rules from DOM
53        this.loadQuestionsFromDOM();
54        this.loadConditionalRules();
55        
56        // Set up event listeners
57        this.bindEvents();
58        
59        // Calculate initial state
60        this.calculateInitialVisibility();
61        this.updateNavigationState();
62        this.updateProgress();
63        
64        // Auto-save setup if enabled
65        if (this.config.autoSaveEnabled) {
66            this.setupAutoSave();
67        }
68        
69        this.log('SB Wizard Conditional Logic initialized successfully');
70    }
71    
72    /**
73     * Alias for init() for backward compatibility
74     */
75    initialize() {
76        return this.init();
77    }
78    
79    /**
80     * Load questions from DOM and build question map
81     */
82    loadQuestionsFromDOM() {
83        const questions = document.querySelectorAll(this.config.questionSelector);
84        
85        questions.forEach((questionEl, index) => {
86            const questionId = this.getQuestionId(questionEl);
87            const questionData = {
88                id: questionId,
89                element: questionEl,
90                step: index,
91                type: this.getQuestionType(questionEl),
92                required: this.isQuestionRequired(questionEl),
93                dependsOn: [],
94                affects: [],
95                visible: true, // Default visibility
96                answered: false
97            };
98            
99            this.questions.set(questionId, questionData);
100            this.visibilityState.set(questionId, true);
101        });
102        
103        this.totalSteps = questions.length;
104        this.log(`Loaded ${questions.length} questions`);
105    }
106    
107    /**
108     * Load conditional rules from backend or DOM data attributes
109     */
110    /**
111     * Load conditional rules from DOM data attributes first, then AJAX
112     */
113    async loadConditionalRules() {
114        try {
115            // First, try to load from DOM data attributes
116            let rulesLoaded = 0;
117            
118            document.querySelectorAll(this.config.questionSelector).forEach(questionEl => {
119                const questionId = this.getQuestionId(questionEl);
120                const conditionalLogicData = questionEl.dataset.conditionalLogic;
121                
122                if (conditionalLogicData) {
123                    try {
124                        // HTML decode the data attribute value
125                        const decodedData = conditionalLogicData
126                            .replace(/"/g, '"')
127                            .replace(/'/g, "'")
128                            .replace(/&lt;/g, '<')
129                            .replace(/&gt;/g, '>')
130                            .replace(/&amp;/g, '&');
131                        
132                        this.log(`Loading conditional logic for question ${questionId}:`, decodedData);
133                        
134                        const rules = JSON.parse(decodedData);
135                        if (rules && rules.enabled) {
136                            this.conditionalRules.set(questionId, rules);
137                            this.buildDependencyMap(questionId, rules);
138                            rulesLoaded++;
139                            this.log(`✅ Conditional logic loaded for question ${questionId}:`, rules);
140                        }
141                    } catch (error) {
142                        this.log(`❌ Error parsing conditional logic for question ${questionId}:`, error);
143                        this.log(`Raw data:`, conditionalLogicData);
144                    }
145                }
146            });
147            
148            // If no DOM rules found, fall back to AJAX loading
149            if (rulesLoaded === 0) {
150                const segmentId = this.getSegmentId();
151                const questionIds = Array.from(this.questions.keys());
152                
153                // Load conditional logic for each question via AJAX
154                for (const questionId of questionIds) {
155                    const rules = await this.fetchConditionalLogic(questionId, segmentId);
156                    if (rules && rules.enabled) {
157                        this.conditionalRules.set(questionId, rules);
158                        this.buildDependencyMap(questionId, rules);
159                        rulesLoaded++;
160                    }
161                }
162            }
163            
164            this.log(`Loaded conditional rules for ${rulesLoaded} questions`);
165        } catch (error) {
166            this.log('Error loading conditional rules:', error);
167        }
168    }
169
170    /**
171     * Add a conditional rule manually (for backward compatibility)
172     */
173    addRule(questionId, ruleData) {
174        try {
175            let rules;
176            if (typeof ruleData === 'string') {
177                rules = JSON.parse(ruleData);
178            } else {
179                rules = ruleData;
180            }
181            
182            if (rules && rules.enabled) {
183                this.conditionalRules.set(questionId, rules);
184                this.buildDependencyMap(questionId, rules);
185                this.log(`Added conditional rule for question ${questionId}`);
186            }
187        } catch (error) {
188            this.log(`Error adding rule for question ${questionId}:`, error);
189        }
190    }
191    
192    /**
193     * Build dependency maps for efficient evaluation
194     */
195    buildDependencyMap(questionId, rules) {
196        const question = this.questions.get(questionId);
197        
198        rules.rules.forEach(rule => {
199            const dependsOnId = rule.depends_on_question;
200            
201            // Add dependency to current question
202            if (!question.dependsOn.includes(dependsOnId)) {
203                question.dependsOn.push(dependsOnId);
204            }
205            
206            // Add reverse dependency to the question being depended on
207            const dependsOnQuestion = this.questions.get(dependsOnId);
208            if (dependsOnQuestion && !dependsOnQuestion.affects.includes(questionId)) {
209                dependsOnQuestion.affects.push(questionId);
210            }
211        });
212    }
213    
214    /**
215     * Set up event listeners for form interactions
216     */
217    bindEvents() {
218        const form = this.getSelector(this.config.formSelector);
219        if (!form) return;
220        
221        // Answer change events
222        form.addEventListener('input', (e) => this.handleAnswerChange(e));
223        form.addEventListener('change', (e) => this.handleAnswerChange(e));
224        
225        // Navigation button events
226        const nextBtn = this.getSelector(this.config.nextButtonSelector);
227        const prevBtn = this.getSelector(this.config.prevButtonSelector);
228        const submitBtn = this.getSelector(this.config.submitButtonSelector);
229        
230        if (nextBtn) {
231            nextBtn.addEventListener('click', (e) => this.handleNext(e));
232        }
233        
234        if (prevBtn) {
235            prevBtn.addEventListener('click', (e) => this.handlePrevious(e));
236        }
237        
238        if (submitBtn) {
239            submitBtn.addEventListener('click', (e) => this.handleSubmit(e));
240        }
241        
242        // Keyboard navigation
243        document.addEventListener('keydown', (e) => this.handleKeyboardNavigation(e));
244    }
245    
246    /**
247     * Handle answer changes with conditional logic evaluation
248     */
249    handleAnswerChange(event) {
250        const questionElement = event.target.closest(this.config.questionSelector);
251        if (!questionElement) return;
252        
253        const questionId = this.getQuestionId(questionElement);
254        const answer = this.getAnswerValue(event.target);
255        
256        // Update answer state
257        this.currentAnswers.set(questionId, answer);
258        
259        // Mark question as answered
260        const question = this.questions.get(questionId);
261        if (question) {
262            question.answered = !!answer;
263        }
264        
265        // Debounce evaluation for performance
266        this.debounceEvaluation(questionId, () => {
267            this.evaluateConditionalLogic(questionId);
268            this.updateNavigationState();
269            this.updateProgress();
270        });
271        
272        // Auto-save if enabled
273        if (this.config.autoSaveEnabled) {
274            this.debounceAutoSave();
275        }
276    }
277    
278    /**
279     * Evaluate conditional logic for affected questions
280     */
281    evaluateConditionalLogic(changedQuestionId) {
282        const question = this.questions.get(changedQuestionId);
283        if (!question) return;
284        
285        this.log(`Evaluating conditional logic for changed question: ${changedQuestionId}`);
286        
287        // Store the current value for the changed question before syncing
288        const currentChangedValue = this.currentAnswers.get(changedQuestionId);
289        console.log(`🔒 Preserving current value for Q${changedQuestionId}: "${currentChangedValue}"`);
290        
291        // Sync all answers from main wizard if available
292        if (typeof window.sbWizard !== 'undefined' && window.sbWizard.answers) {
293            Object.entries(window.sbWizard.answers).forEach(([qId, answer]) => {
294                // Skip the changed question to preserve its fresh value
295                if (qId == changedQuestionId) {
296                    console.log(`⏭️ Skipping sync for changed question Q${qId} to preserve fresh value`);
297                    return;
298                }
299                
300                if (answer !== undefined && answer !== null && answer !== '') {
301                    this.currentAnswers.set(qId, answer);
302                    this.log(`Synced answer from main wizard: question ${qId} = "${answer}"`);
303                }
304            });
305        }
306        
307        // Restore the current value for the changed question
308        if (currentChangedValue !== undefined) {
309            this.currentAnswers.set(changedQuestionId, currentChangedValue);
310            console.log(`🔄 Restored fresh value for Q${changedQuestionId}: "${currentChangedValue}"`);
311        }
312        
313        // When a question changes, re-evaluate ALL questions with conditional logic
314        // because any question could potentially be affected
315        this.conditionalRules.forEach((rules, questionId) => {
316            this.evaluateQuestionVisibility(questionId);
317        });
318        
319        // Update visible questions list
320        this.updateVisibleQuestionsList();
321        
322        // DO NOT auto-show questions - let the main wizard handle navigation
323        // Only update visibility state, don't change display
324    }
325    
326    /**
327     * Evaluate all conditional rules (convenience method)
328     */
329    evaluateAllRules() {
330        this.log('=== Evaluating all conditional rules ===');
331        for (const questionId of this.conditionalRules.keys()) {
332            this.evaluateQuestionVisibility(questionId);
333        }
334        
335        // Force DOM update completion before updating navigation
336        setTimeout(() => {
337            this.updateVisibleQuestionsList();
338            this.log('All conditional rules evaluated - DOM updated');
339            
340            // Trigger wizard navigation update after all DOM changes are complete
341            if (typeof window.sbWizard !== 'undefined' && window.sbWizard.updateNavigation) {
342                setTimeout(() => {
343                    console.log('🔄 Final wizard navigation update after all conditional logic evaluation');
344                    window.sbWizard.updateNavigation();
345                }, 10);
346            }
347        }, 10);
348    }
349    
350    /**
351     * Evaluate visibility for a specific question
352     */
353    evaluateQuestionVisibility(questionId) {
354        const rules = this.conditionalRules.get(questionId);
355        if (!rules || !rules.enabled) {
356            // No rules - question should remain visible by default
357            this.setQuestionVisibility(questionId, true);
358            return;
359        }
360        
361        let isVisible = false;
362        
363        if (rules.logic === 'OR') {
364            // OR logic: any rule passes = visible
365            isVisible = rules.rules.some(rule => this.evaluateRule(rule));
366        } else {
367            // AND logic: all rules must pass = visible
368            isVisible = rules.rules.every(rule => this.evaluateRule(rule));
369        }
370        
371        this.log(`Evaluating question ${questionId}: rules=${JSON.stringify(rules)}, result=${isVisible}`);
372        
373        // CRITICAL DEBUG: Check if we reach this point  
374        console.log(`🚨 CRITICAL: About to call setQuestionVisibility(${questionId}, ${isVisible})`);
375        
376        // Debug logging before calling setQuestionVisibility
377        console.log(`🎯 About to call setQuestionVisibility(${questionId}, ${isVisible})`);
378        
379        // Apply visibility change
380        this.setQuestionVisibility(questionId, isVisible);
381        
382        console.log(`✅ setQuestionVisibility(${questionId}, ${isVisible}) completed`);
383    }
384    
385    /**
386     * Evaluate a single conditional rule
387     */
388    evaluateRule(rule) {
389        const dependsOnAnswer = this.currentAnswers.get(rule.depends_on_question);
390        
391        this.log(`Evaluating rule: depends_on_question=${rule.depends_on_question}, operator=${rule.operator}, value="${rule.value}", actual_answer="${dependsOnAnswer}"`);
392        console.log(`🔍 RULE DEBUG: Q${rule.depends_on_question} actual="${dependsOnAnswer}" vs expected="${rule.value}" (${rule.operator})`);
393        
394        if (dependsOnAnswer === undefined || dependsOnAnswer === null) {
395            this.log(`No answer found for question ${rule.depends_on_question}, rule fails`);
396            console.log(`❌ Rule fails: No answer for Q${rule.depends_on_question}`);
397            return false; // No answer yet
398        }
399        
400        // Handle array values (checkbox questions)
401        const isArrayAnswer = Array.isArray(dependsOnAnswer);
402        this.log(`Answer is array: ${isArrayAnswer}, value: ${JSON.stringify(dependsOnAnswer)}`);
403        
404        let result = false;
405        switch (rule.operator) {
406            case 'equals':
407                if (isArrayAnswer) {
408                    // For checkbox arrays, check if the value exists in the array
409                    result = dependsOnAnswer.includes(rule.value.toString());
410                    this.log(`Array equals check: ${JSON.stringify(dependsOnAnswer)}.includes("${rule.value}") = ${result}`);
411                } else {
412                    result = dependsOnAnswer.toString() === rule.value.toString();
413                }
414                break;
415                
416            case 'not_equals':
417                if (isArrayAnswer) {
418                    // For checkbox arrays, check if the value does NOT exist in the array
419                    result = !dependsOnAnswer.includes(rule.value.toString());
420                    this.log(`Array not_equals check: !${JSON.stringify(dependsOnAnswer)}.includes("${rule.value}") = ${result}`);
421                } else {
422                    result = dependsOnAnswer.toString() !== rule.value.toString();
423                }
424                break;
425                
426            case 'not_empty':
427                if (isArrayAnswer) {
428                    // For arrays, check if array has any elements
429                    result = dependsOnAnswer.length > 0;
430                    this.log(`Array not_empty check: ${JSON.stringify(dependsOnAnswer)}.length > 0 = ${result}`);
431                } else {
432                    result = dependsOnAnswer !== '' && dependsOnAnswer !== null;
433                }
434                break;
435                
436            case 'contains':
437                if (isArrayAnswer) {
438                    // For arrays, check if any element contains the search value
439                    result = dependsOnAnswer.some(item => item.toString().toLowerCase().includes(rule.value.toString().toLowerCase()));
440                    this.log(`Array contains check: some item in ${JSON.stringify(dependsOnAnswer)} contains "${rule.value}" = ${result}`);
441                } else {
442                    result = dependsOnAnswer.toString().toLowerCase().includes(rule.value.toString().toLowerCase());
443                }
444                break;
445                
446            case 'greater_than':
447                if (isArrayAnswer) {
448                    // For arrays, check if any numeric value in array is greater than rule value
449                    result = dependsOnAnswer.some(item => parseFloat(item) > parseFloat(rule.value));
450                    this.log(`Array greater_than check: some item in ${JSON.stringify(dependsOnAnswer)} > ${rule.value} = ${result}`);
451                } else {
452                    result = parseFloat(dependsOnAnswer) > parseFloat(rule.value);
453                }
454                break;
455                
456            case 'less_than':
457                if (isArrayAnswer) {
458                    // For arrays, check if any numeric value in array is less than rule value
459                    result = dependsOnAnswer.some(item => parseFloat(item) < parseFloat(rule.value));
460                    this.log(`Array less_than check: some item in ${JSON.stringify(dependsOnAnswer)} < ${rule.value} = ${result}`);
461                } else {
462                    result = parseFloat(dependsOnAnswer) < parseFloat(rule.value);
463                }
464                break;
465                
466            case 'greater_than_or_equal':
467                if (isArrayAnswer) {
468                    // For arrays, check if any numeric value in array is greater than or equal to rule value
469                    result = dependsOnAnswer.some(item => parseFloat(item) >= parseFloat(rule.value));
470                    this.log(`Array greater_than_or_equal check: some item in ${JSON.stringify(dependsOnAnswer)} >= ${rule.value} = ${result}`);
471                } else {
472                    result = parseFloat(dependsOnAnswer) >= parseFloat(rule.value);
473                }
474                break;
475                
476            case 'less_than_or_equal':
477                if (isArrayAnswer) {
478                    // For arrays, check if any numeric value in array is less than or equal to rule value
479                    result = dependsOnAnswer.some(item => parseFloat(item) <= parseFloat(rule.value));
480                    this.log(`Array less_than_or_equal check: some item in ${JSON.stringify(dependsOnAnswer)} <= ${rule.value} = ${result}`);
481                } else {
482                    result = parseFloat(dependsOnAnswer) <= parseFloat(rule.value);
483                }
484                break;
485                
486            case 'between':
487                if (Array.isArray(rule.value) && rule.value.length === 2) {
488                    const minValue = parseFloat(rule.value[0]);
489                    const maxValue = parseFloat(rule.value[1]);
490                    if (isArrayAnswer) {
491                        // For arrays, check if any numeric value in array is between min and max
492                        result = dependsOnAnswer.some(item => {
493                            const numValue = parseFloat(item);
494                            return numValue >= minValue && numValue <= maxValue;
495                        });
496                        this.log(`Array between check: some item in ${JSON.stringify(dependsOnAnswer)} between ${minValue} and ${maxValue} = ${result}`);
497                    } else {
498                        const numValue = parseFloat(dependsOnAnswer);
499                        result = numValue >= minValue && numValue <= maxValue;
500                        this.log(`Between check: ${numValue} between ${minValue} and ${maxValue} = ${result}`);
501                    }
502                } else {
503                    this.log(`Invalid between rule value: ${JSON.stringify(rule.value)}, expected array with 2 elements`);
504                    result = false;
505                }
506                break;
507                
508            default:
509                this.log(`Unknown operator: ${rule.operator}`);
510                result = false;
511        }
512        
513        this.log(`Rule evaluation result: ${result} (${dependsOnAnswer} ${rule.operator} ${rule.value})`);
514        return result;
515    }
516    
517    /**
518     * Set question visibility with smooth animations
519     */
520    setQuestionVisibility(questionId, visible) {
521        console.log(`🔧 setQuestionVisibility(${questionId}, ${visible}) called`);
522        
523        const previousVisibility = this.visibilityState.get(questionId);
524        console.log(`Previous visibility for Q${questionId}: ${previousVisibility}`);
525        
526        const question = this.questions.get(questionId);
527        console.log(`Question object found for Q${questionId}:`, !!question);
528        if (!question) {
529            console.log(`❌ No question found for Q${questionId} in questions map`);
530            return;
531        }
532
533        // Always update DOM attribute and visibility state - don't skip if values match
534        // This ensures DOM is in sync even if state tracking shows no change
535        this.visibilityState.set(questionId, visible);
536        console.log(`🔄 Forced update: Setting Q${questionId} visibility state to ${visible}`);        // Update data attribute for wizard navigation - proper DOM handling
537        const questionEl = question.element;
538        if (visible) {
539            questionEl.removeAttribute('data-conditional-hidden');
540        } else {
541            questionEl.setAttribute('data-conditional-hidden', 'true');
542        }
543        
544        // Force DOM flush for navigation update
545        questionEl.offsetHeight; // Force reflow
546        
547        // Update jQuery data for compatibility
548        if (window.jQuery) {
549            window.jQuery(questionEl).data('conditional-hidden', visible ? null : 'true');
550        }
551        
552        this.log(`Question ${questionId} visibility: ${visible ? 'visible' : 'hidden'} (data-conditional-hidden=${questionEl.getAttribute('data-conditional-hidden') || 'null'})`);
553        
554        // Debug DOM attribute setting
555        console.log(`🔧 DOM DEBUG Q${questionId}: element exists=${!!questionEl}, attribute set=${questionEl.getAttribute('data-conditional-hidden')}, visible=${visible}`);
556        console.log(`🔧 Element classes:`, questionEl.className);
557        console.log(`🔧 Element data-question-id:`, questionEl.getAttribute('data-question-id'));
558        
559        // Update debug info if available
560        this.updateDebugInfo(questionId, visible);
561        
562        // Trigger wizard navigation update after DOM changes
563        if (typeof window.sbWizard !== 'undefined' && window.sbWizard.updateNavigation) {
564            setTimeout(() => {
565                console.log(`🔄 Triggering wizard navigation update for question ${questionId} visibility change`);
566                window.sbWizard.updateNavigation();
567            }, 10); // Reduced delay since DOM handling is now fixed
568        }
569    }
570    
571    /**
572     * Update debug information in the question HTML
573     */
574    updateDebugInfo(questionId, isVisible) {
575        const question = this.questions.get(questionId);
576        if (!question) return;
577        
578        const debugDiv = question.element.querySelector('.sb-wizard-debug-info');
579        if (!debugDiv) return;
580        
581        // Update visibility status
582        const visibilitySpan = debugDiv.querySelector('.debug-visibility-status');
583        if (visibilitySpan) {
584            visibilitySpan.textContent = isVisible ? 'VISIBLE ✅' : 'HIDDEN ❌';
585            visibilitySpan.style.color = isVisible ? 'green' : 'red';
586            visibilitySpan.style.fontWeight = 'bold';
587        }
588        
589        // Update last evaluation time
590        const lastEvalSpan = debugDiv.querySelector('.debug-last-eval');
591        if (lastEvalSpan) {
592            lastEvalSpan.textContent = new Date().toLocaleTimeString();
593        }
594        
595        // Update current answer
596        const answerSpan = debugDiv.querySelector('.debug-current-answer');
597        if (answerSpan) {
598            const currentAnswer = this.currentAnswers.get(questionId);
599            answerSpan.textContent = currentAnswer !== undefined ? JSON.stringify(currentAnswer) : 'null';
600        }
601        
602        // Update dependencies info
603        const depsSpan = debugDiv.querySelector('.debug-dependencies');
604        if (depsSpan) {
605            const rules = this.conditionalRules.get(questionId);
606            if (rules && rules.rules) {
607                const depsInfo = rules.rules.map(rule => {
608                    const depAnswer = this.currentAnswers.get(rule.depends_on_question);
609                    const ruleResult = this.evaluateRule(rule);
610                    return `Q${rule.depends_on_question}="${depAnswer}" ${rule.operator} "${rule.value}" = ${ruleResult ? '✅' : '❌'}`;
611                }).join(' | ');
612                depsSpan.innerHTML = `<br><strong>Rule Evaluation:</strong> ${depsInfo}`;
613                
614                // Show overall result
615                const overallResult = rules.logic === 'OR' 
616                    ? rules.rules.some(rule => this.evaluateRule(rule))
617                    : rules.rules.every(rule => this.evaluateRule(rule));
618                depsSpan.innerHTML += `<br><strong>Overall (${rules.logic}):</strong> ${overallResult ? 'SHOW ✅' : 'HIDE ❌'}`;
619            } else {
620                depsSpan.textContent = 'None';
621            }
622        }
623        
624        // Update all answers debug info
625        const allAnswersSpan = debugDiv.querySelector('.debug-all-answers');
626        if (allAnswersSpan) {
627            const allAnswers = [];
628            for (const [qId, answer] of this.currentAnswers.entries()) {
629                allAnswers.push(`Q${qId}="${answer}"`);
630            }
631            allAnswersSpan.textContent = allAnswers.length > 0 ? allAnswers.join(', ') : 'No answers stored';
632        }
633    }
634    
635    /**
636     * Show question with animation
637     */
638    showQuestion(element) {
639        element.style.display = 'block';
640        element.style.opacity = '0';
641        element.style.transform = 'translateY(20px)';
642        
643        // Trigger animation
644        requestAnimationFrame(() => {
645            element.style.transition = `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease`;
646            element.style.opacity = '1';
647            element.style.transform = 'translateY(0)';
648        });
649        
650        // Update tab index
651        this.updateTabIndex(element, false);
652    }
653    
654    /**
655     * Hide question with animation
656     */
657    hideQuestion(element) {
658        element.style.transition = `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease`;
659        element.style.opacity = '0';
660        element.style.transform = 'translateY(-20px)';
661        
662        setTimeout(() => {
663            element.style.display = 'none';
664        }, this.config.animationDuration);
665        
666        // Update tab index
667        this.updateTabIndex(element, true);
668    }
669    
670    /**
671     * Clear answer for hidden question
672     */
673    clearQuestionAnswer(questionId) {
674        const question = this.questions.get(questionId);
675        if (!question) return;
676        
677        // Clear answer value
678        this.currentAnswers.delete(questionId);
679        question.answered = false;
680        
681        // Clear form inputs
682        const inputs = question.element.querySelectorAll('input, select, textarea');
683        inputs.forEach(input => {
684            if (input.type === 'checkbox' || input.type === 'radio') {
685                input.checked = false;
686            } else {
687                input.value = '';
688            }
689        });
690    }
691    
692    /**
693     * Update visible questions list for navigation
694     */
695    updateVisibleQuestionsList() {
696        this.visibleQuestions = Array.from(this.questions.values())
697            .filter(q => this.visibilityState.get(q.id))
698            .sort((a, b) => a.step - b.step);
699        
700        this.requiredQuestions = this.visibleQuestions
701            .filter(q => q.required);
702    }
703    
704    /**
705     * Update navigation button states
706     */
707    updateNavigationState() {
708        this.updateVisibleQuestionsList();
709        
710        // Calculate current position in visible questions
711        const currentQuestion = this.getCurrentQuestion();
712        const currentIndex = this.visibleQuestions.findIndex(q => q.id === currentQuestion?.id);
713        
714        // Update navigation flags
715        this.canGoPrev = currentIndex > 0;
716        this.canGoNext = this.canProceedNext(currentIndex);
717        
718        // Update button states
719        this.updateNavigationButtons();
720        
721        this.log(`Navigation state: prev=${this.canGoPrev}, next=${this.canGoNext}, step=${currentIndex + 1}/${this.visibleQuestions.length}`);
722    }
723    
724    /**
725     * Check if user can proceed to next question
726     */
727    canProceedNext(currentIndex) {
728        if (currentIndex < 0 || currentIndex >= this.visibleQuestions.length - 1) {
729            return false; // At last question or invalid position
730        }
731        
732        // Check if current question is answered (if required)
733        const currentQuestion = this.visibleQuestions[currentIndex];
734        if (currentQuestion?.required) {
735            return currentQuestion.answered && this.isAnswerValid(currentQuestion.id);
736        }
737        
738        return true; // Can proceed if not required or answered
739    }
740    
741    /**
742     * Update navigation button visual states
743     */
744    updateNavigationButtons() {
745        // TEMPORARILY DISABLED - Main wizard handles navigation
746        // The conditional logic and main wizard navigation were conflicting
747        console.log('[SB Wizard] Navigation buttons update skipped - main wizard handles this');
748        return;
749        
750        const nextBtn = this.getSelector(this.config.nextButtonSelector);
751        const prevBtn = this.getSelector(this.config.prevButtonSelector);
752        const submitBtn = this.getSelector(this.config.submitButtonSelector);
753        
754        // Previous button
755        if (prevBtn) {
756            prevBtn.disabled = !this.canGoPrev;
757            prevBtn.classList.toggle('disabled', !this.canGoPrev);
758        }
759        
760        // Next/Submit button logic
761        const isLastQuestion = this.isAtLastQuestion();
762        
763        if (isLastQuestion) {
764            // Show submit button, hide next
765            if (nextBtn) {
766                nextBtn.style.display = 'none';
767            }
768            if (submitBtn) {
769                submitBtn.style.display = 'inline-block';
770                submitBtn.disabled = !this.canSubmit();
771                submitBtn.classList.toggle('disabled', !this.canSubmit());
772            }
773        } else {
774            // Show next button, hide submit
775            if (nextBtn) {
776                nextBtn.style.display = 'inline-block';
777                nextBtn.disabled = !this.canGoNext;
778                nextBtn.classList.toggle('disabled', !this.canGoNext);
779            }
780            if (submitBtn) {
781                submitBtn.style.display = 'none';
782            }
783        }
784    }
785    
786    /**
787     * Handle next button click
788     */
789    handleNext(event) {
790        event.preventDefault();
791        
792        if (!this.canGoNext) {
793            this.showValidationMessage('Please complete the current question before proceeding.');
794            return;
795        }
796        
797        const currentIndex = this.getCurrentQuestionIndex();
798        if (currentIndex >= 0 && currentIndex < this.visibleQuestions.length - 1) {
799            this.navigateToQuestion(currentIndex + 1);
800            this.addToNavigationHistory(currentIndex + 1);
801        }
802    }
803    
804    /**
805     * Handle previous button click
806     */
807    handlePrevious(event) {
808        event.preventDefault();
809        
810        if (!this.canGoPrev) {
811            return;
812        }
813        
814        const currentIndex = this.getCurrentQuestionIndex();
815        if (currentIndex > 0) {
816            this.navigateToQuestion(currentIndex - 1);
817            this.addToNavigationHistory(currentIndex - 1);
818        }
819    }
820    
821    /**
822     * Navigate to specific question index
823     */
824    navigateToQuestion(targetIndex) {
825        if (targetIndex < 0 || targetIndex >= this.visibleQuestions.length) {
826            return;
827        }
828        
829        // Hide current question
830        const currentQuestion = this.getCurrentQuestion();
831        if (currentQuestion) {
832            currentQuestion.element.classList.remove('active');
833        }
834        
835        // Show target question
836        const targetQuestion = this.visibleQuestions[targetIndex];
837        targetQuestion.element.classList.add('active');
838        targetQuestion.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
839        
840        // Update current step
841        this.currentStep = targetIndex;
842        
843        // Update states
844        this.updateNavigationState();
845        this.updateProgress();
846        
847        this.log(`Navigated to question ${targetIndex + 1}/${this.visibleQuestions.length}`);
848    }
849    
850    /**
851     * Handle form submission
852     */
853    async handleSubmit(event) {
854        event.preventDefault();
855        
856        if (!this.canSubmit()) {
857            this.showValidationErrors();
858            return;
859        }
860        
861        try {
862            const formData = this.collectFormData();
863            await this.submitForm(formData);
864        } catch (error) {
865            this.log('Submission error:', error);
866            this.showErrorMessage('Submission failed. Please try again.');
867        }
868    }
869    
870    /**
871     * Check if form can be submitted
872     */
873    canSubmit() {
874        // All required visible questions must be answered
875        return this.requiredQuestions.every(question => 
876            question.answered && this.isAnswerValid(question.id)
877        );
878    }
879    
880    /**
881     * Update progress indicator
882     */
883    updateProgress() {
884        const progressEl = this.getSelector(this.config.progressSelector);
885        if (!progressEl) return;
886        
887        const answered = this.visibleQuestions.filter(q => q.answered).length;
888        const total = this.visibleQuestions.length;
889        const percentage = total > 0 ? Math.round((answered / total) * 100) : 0;
890        
891        progressEl.style.width = `${percentage}%`;
892        progressEl.setAttribute('aria-valuenow', percentage);
893        progressEl.textContent = `${percentage}% Complete`;
894        
895        this.log(`Progress: ${answered}/${total} questions (${percentage}%)`);
896    }
897    
898    /**
899     * Auto-save functionality
900     */
901    setupAutoSave() {
902        this.autoSaveTimer = null;
903    }
904    
905    debounceAutoSave() {
906        if (this.autoSaveTimer) {
907            clearTimeout(this.autoSaveTimer);
908        }
909        
910        this.autoSaveTimer = setTimeout(() => {
911            this.performAutoSave();
912        }, this.config.autoSaveDelay);
913    }
914    
915    async performAutoSave() {
916        try {
917            const formData = this.collectFormData();
918            await this.saveProgress(formData);
919            this.showSaveStatus('Saved automatically');
920        } catch (error) {
921            this.log('Auto-save error:', error);
922        }
923    }
924    
925    /**
926     * Utility functions
927     */
928    getQuestionId(element) {
929        return element.dataset.questionId || element.id;
930    }
931    
932    getQuestionType(element) {
933        return element.dataset.questionType || 'text';
934    }
935    
936    isQuestionRequired(element) {
937        return element.dataset.required === 'true' || element.required;
938    }
939    
940    getSegmentId() {
941        const form = this.getSelector(this.config.formSelector);
942        return form?.dataset.segmentId || 1;
943    }
944    
945    getAnswerValue(input) {
946        if (input.type === 'checkbox') {
947            return input.checked ? input.value : '';
948        }
949        if (input.type === 'radio') {
950            const form = input.closest('form');
951            const checked = form?.querySelector(`input[name="${input.name}"]:checked`);
952            return checked ? checked.value : '';
953        }
954        return input.value;
955    }
956    
957    getCurrentQuestion() {
958        return this.visibleQuestions[this.currentStep];
959    }
960    
961    getCurrentQuestionIndex() {
962        return this.currentStep;
963    }
964    
965    isAtLastQuestion() {
966        return this.currentStep >= this.visibleQuestions.length - 1;
967    }
968    
969    getSelector(selector) {
970        if (!this.cachedSelectors.has(selector)) {
971            this.cachedSelectors.set(selector, document.querySelector(selector));
972        }
973        return this.cachedSelectors.get(selector);
974    }
975    
976    debounceEvaluation(questionId, callback) {
977        if (this.debounceTimers.has(questionId)) {
978            clearTimeout(this.debounceTimers.get(questionId));
979        }
980        
981        this.debounceTimers.set(questionId, setTimeout(callback, 100));
982    }
983    
984    addToNavigationHistory(index) {
985        this.navigationHistory.push(index);
986        if (this.navigationHistory.length > 10) {
987            this.navigationHistory.shift();
988        }
989    }
990    
991    /**
992     * API calls
993     */
994    async fetchConditionalLogic(questionId, segmentId) {
995        try {
996            const response = await fetch(sbWizardAjax.ajaxurl, {
997                method: 'POST',
998                headers: {
999                    'Content-Type': 'application/x-www-form-urlencoded',
1000                },
1001                body: new URLSearchParams({
1002                    action: 'sbw_get_question_conditional_logic',
1003                    nonce: sbWizardAjax.nonce,
1004                    question_id: questionId,
1005                    segment_id: segmentId
1006                })
1007            });
1008            
1009            const data = await response.json();
1010            return data.success ? data.data : null;
1011        } catch (error) {
1012            this.log('Failed to fetch conditional logic:', error);
1013            return null;
1014        }
1015    }
1016    
1017    calculateInitialVisibility() {
1018        // Evaluate all conditional rules on page load
1019        this.conditionalRules.forEach((rules, questionId) => {
1020            this.evaluateQuestionVisibility(questionId);
1021        });
1022        
1023        this.updateVisibleQuestionsList();
1024    }
1025    
1026    /**
1027     * Validation functions
1028     */
1029    isAnswerValid(questionId) {
1030        const question = this.questions.get(questionId);
1031        if (!question) return false;
1032        
1033        const answer = this.currentAnswers.get(questionId);
1034        
1035        // Check if required question has answer
1036        if (question.required && (!answer || answer === '')) {
1037            return false;
1038        }
1039        
1040        // Type-specific validation
1041        switch (question.type) {
1042            case 'email':
1043                return !answer || this.validateEmail(answer);
1044            case 'phone':
1045                return !answer || this.validatePhone(answer);
1046            case 'number':
1047                return !answer || this.validateNumber(answer);
1048            case 'url':
1049                return !answer || this.validateUrl(answer);
1050            default:
1051                return true;
1052        }
1053    }
1054    
1055    validateEmail(email) {
1056        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1057        return emailRegex.test(email);
1058    }
1059    
1060    validatePhone(phone) {
1061        const phoneRegex = /^[\+]?[(]?[0-9\s\-\(\)]{10,}$/;
1062        return phoneRegex.test(phone.replace(/\s/g, ''));
1063    }
1064    
1065    validateNumber(number) {
1066        return !isNaN(parseFloat(number)) && isFinite(number);
1067    }
1068    
1069    validateUrl(url) {
1070        try {
1071            new URL(url);
1072            return true;
1073        } catch {
1074            return false;
1075        }
1076    }
1077    
1078    showValidationMessage(message, type = 'error') {
1079        const messageEl = document.createElement('div');
1080        messageEl.className = `sb-${type}-message`;
1081        messageEl.textContent = message;
1082        
1083        // Remove existing messages
1084        document.querySelectorAll('.sb-error-message, .sb-success-message').forEach(el => el.remove());
1085        
1086        // Add new message
1087        const form = this.getSelector(this.config.formSelector);
1088        if (form) {
1089            form.insertBefore(messageEl, form.firstChild);
1090            
1091            // Auto-remove after 5 seconds
1092            setTimeout(() => messageEl.remove(), 5000);
1093        }
1094    }
1095    
1096    showValidationErrors() {
1097        const errors = [];
1098        
1099        this.requiredQuestions.forEach(question => {
1100            if (!question.answered || !this.isAnswerValid(question.id)) {
1101                errors.push(`Please complete: ${this.getQuestionText(question)}`);
1102                this.highlightQuestionError(question);
1103            }
1104        });
1105        
1106        if (errors.length > 0) {
1107            this.showValidationMessage(`Please complete all required fields:\n${errors.join('\n')}`);
1108        }
1109    }
1110    
1111    highlightQuestionError(question) {
1112        question.element.classList.add('invalid');
1113        question.element.classList.remove('valid');
1114        
1115        // Remove error class after user interacts
1116        const removeError = () => {
1117            question.element.classList.remove('invalid');
1118            question.element.removeEventListener('input', removeError);
1119            question.element.removeEventListener('change', removeError);
1120        };
1121        
1122        question.element.addEventListener('input', removeError);
1123        question.element.addEventListener('change', removeError);
1124    }
1125    
1126    highlightQuestionValid(question) {
1127        question.element.classList.add('valid');
1128        question.element.classList.remove('invalid');
1129    }
1130    
1131    getQuestionText(question) {
1132        const label = question.element.querySelector('label, .question-text');
1133        return label ? label.textContent.trim() : `Question ${question.step + 1}`;
1134    }
1135    
1136    showSaveStatus(message, isError = false) {
1137        let statusEl = document.querySelector('.sb-save-status');
1138        if (!statusEl) {
1139            statusEl = document.createElement('div');
1140            statusEl.className = 'sb-save-status';
1141            document.body.appendChild(statusEl);
1142        }
1143        
1144        statusEl.textContent = message;
1145        statusEl.className = `sb-save-status ${isError ? 'error' : ''} visible`;
1146        
1147        // Hide after 3 seconds
1148        setTimeout(() => {
1149            statusEl.classList.remove('visible');
1150        }, 3000);
1151    }
1152    
1153    showErrorMessage(message) {
1154        this.showValidationMessage(message, 'error');
1155    }
1156    
1157    updateTabIndex(element, disabled) {
1158        const inputs = element.querySelectorAll('input, select, textarea, button');
1159        inputs.forEach(input => {
1160            input.tabIndex = disabled ? -1 : 0;
1161        });
1162    }
1163    
1164    handleKeyboardNavigation(event) {
1165        // Ctrl/Cmd + Arrow keys for navigation
1166        if ((event.ctrlKey || event.metaKey)) {
1167            if (event.key === 'ArrowRight' && this.canGoNext) {
1168                event.preventDefault();
1169                this.handleNext(event);
1170            } else if (event.key === 'ArrowLeft' && this.canGoPrev) {
1171                event.preventDefault();
1172                this.handlePrevious(event);
1173            }
1174        }
1175        
1176        // Enter key to proceed
1177        if (event.key === 'Enter' && !event.target.matches('textarea')) {
1178            if (this.isAtLastQuestion() && this.canSubmit()) {
1179                event.preventDefault();
1180                this.handleSubmit(event);
1181            } else if (this.canGoNext) {
1182                event.preventDefault();
1183                this.handleNext(event);
1184            }
1185        }
1186    }
1187    
1188    /**
1189     * Data collection and submission
1190     */
1191    collectFormData() {
1192        const formData = {
1193            segment_id: this.getSegmentId(),
1194            answers: Object.fromEntries(this.currentAnswers),
1195            visible_questions: this.visibleQuestions.map(q => q.id),
1196            navigation_history: this.navigationHistory,
1197            completion_percentage: this.calculateCompletionPercentage(),
1198            timestamp: new Date().toISOString()
1199        };
1200        
1201        return formData;
1202    }
1203    
1204    calculateCompletionPercentage() {
1205        const answered = this.visibleQuestions.filter(q => q.answered).length;
1206        const total = this.visibleQuestions.length;
1207        return total > 0 ? Math.round((answered / total) * 100) : 0;
1208    }
1209    
1210    async saveProgress(formData) {
1211        const response = await fetch(sbWizardAjax.ajaxurl, {
1212            method: 'POST',
1213            headers: {
1214                'Content-Type': 'application/x-www-form-urlencoded',
1215            },
1216            body: new URLSearchParams({
1217                action: 'sbw_save_progress',
1218                nonce: sbWizardAjax.nonce,
1219                form_data: JSON.stringify(formData)
1220            })
1221        });
1222        
1223        if (!response.ok) {
1224            throw new Error('Save failed');
1225        }
1226        
1227        return response.json();
1228    }
1229    
1230    async submitForm(formData) {
1231        // Show loading state
1232        const submitBtn = this.getSelector(this.config.submitButtonSelector);
1233        if (submitBtn) {
1234            submitBtn.classList.add('sb-loading');
1235            submitBtn.disabled = true;
1236            submitBtn.textContent = 'Submitting...';
1237        }
1238        
1239        try {
1240            const response = await fetch(sbWizardAjax.ajaxurl, {
1241                method: 'POST',
1242                headers: {
1243                    'Content-Type': 'application/x-www-form-urlencoded',
1244                },
1245                body: new URLSearchParams({
1246                    action: 'sbw_submit_form',
1247                    nonce: sbWizardAjax.nonce,
1248                    segment_id: this.config.segmentId,
1249                    form_data: JSON.stringify(formData)
1250                })
1251            });
1252            
1253            const result = await response.json();
1254            
1255            if (result.success) {
1256                this.handleSuccessfulSubmission(result.data);
1257            } else {
1258                throw new Error(result.data || 'Submission failed');
1259            }
1260        } catch (error) {
1261            this.log('Submission error:', error);
1262            this.handleSubmissionError(error);
1263        }
1264    }
1265    
1266    /**
1267     * Handle successful form submission
1268     */
1269    handleSuccessfulSubmission(data) {
1270        // Hide the entire form
1271        const formContainer = this.getSelector('.sb-wizard-form');
1272        if (formContainer) {
1273            formContainer.style.display = 'none';
1274        }
1275        
1276        // Show completion message
1277        this.showCompletionMessage(data);
1278        
1279        // Redirect after delay if URL provided
1280        if (data.redirect_url) {
1281            setTimeout(() => {
1282                window.location.href = data.redirect_url;
1283            }, 2000);
1284        }
1285    }
1286    
1287    /**
1288     * Handle submission errors
1289     */
1290    handleSubmissionError(error) {
1291        const submitBtn = this.getSelector(this.config.submitButtonSelector);
1292        if (submitBtn) {
1293            submitBtn.classList.remove('sb-loading');
1294            submitBtn.disabled = false;
1295            submitBtn.textContent = 'Submit';
1296        }
1297        
1298        this.showValidationMessage('Submission failed. Please try again.', 'error');
1299        this.log('Submission error:', error);
1300    }
1301    
1302    /**
1303     * Show completion message
1304     */
1305    showCompletionMessage(data) {
1306        const container = this.getSelector('.sb-wizard-container') || document.body;
1307        
1308        const completionDiv = document.createElement('div');
1309        completionDiv.className = 'sb-wizard-completion';
1310        completionDiv.innerHTML = `
1311            <div class="completion-content">
1312                <div class="completion-icon">✅</div>
1313                <h2>Thank You!</h2>
1314                <p>Your responses have been submitted successfully.</p>
1315                ${data.message ? `<p class="completion-message">${data.message}</p>` : ''}
1316                ${data.recommendations ? `<div class="recommendations">${data.recommendations}</div>` : ''}
1317                ${data.redirect_url ? '<p class="redirect-notice">Redirecting you now...</p>' : ''}
1318            </div>
1319        `;
1320        
1321        container.appendChild(completionDiv);
1322        
1323        // Animate in
1324        setTimeout(() => {
1325            completionDiv.classList.add('show');
1326        }, 100);
1327    }
1328    
1329    log(...args) {
1330        if (this.config.debugMode) {
1331            console.log('[SB Wizard]', ...args);
1332        }
1333    }
1334}
1335
1336// Auto-initialize when DOM is ready
1337document.addEventListener('DOMContentLoaded', () => {
1338    if (document.querySelector('.sb-wizard-form')) {
1339        window.sbWizardConditionalLogic = new SBWizardConditionalLogic({
1340            debugMode: true // Enable for development
1341        });
1342    }
1343});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.