PageSourceSearch

https://globalliver.org/wp-content/plugins/quiz-master-next/render…/js/qsm-quiz-navigation.js?ver=11.2.2

js globalliver.org collected 2026-10-02 04:55:55 UTC 109,540 bytes, 2,294 lines download raw bytes

1/**
2 * QSM-11 Quiz Navigation JavaScript
3 * 
4 * Handles navigation, pagination, and validation functionality
5 * for the new QSM-11 rendering system
6 * 
7 * @package QSM
8 * @version 1.0.0
9 */
10var show_result_validation = true;
11var QSMPagination;
12
13/**
14 * Limit multiple response based on question limit.
15 *
16 * Defined globally because the new renderer's question templates emit an inline
17 * `onchange="qsmCheckMR(this, limit)"` handler. The legacy definition lives in
18 * js/qsm-quiz.js, which the new renderer does not enqueue, so it is redefined here.
19 *
20 * @returns
21 */
22if (typeof window.qsmCheckMR !== 'function') {
23    window.qsmCheckMR = function(event, limit) {
24        let $section = jQuery(event).parents('.quiz_section');
25        let checked = $section.find(':checkbox:checked').length;
26        if (checked > limit) {
27            event.checked = false;
28            if ($section.find('.multi-checkbox-limit-reach').length == 0) {
29                let message = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.multicheckbox_limit_reach) ? qmn_ajax_object.multicheckbox_limit_reach : '';
30                $section.append('<span class="multi-checkbox-limit-reach">' + message + '</span>');
31            }
32        } else {
33            $section.find('.multi-checkbox-limit-reach').remove();
34        }
35    };
36}
37
38(function($) {
39    /**
40     * Main QSM New Navigation Object
41     */
42    QSMPagination = {
43        Navigation: {
44            quizObjects: {},
45            timeTakenIntervals: {},
46            videoAttributePatterns: [
47                /\ssrc="([^"]+)"/,
48                /\smp4="([^"]+)"/,
49                /\sm4v="([^"]+)"/,
50                /\swebm="([^"]+)"/,
51                /\sogv="([^"]+)"/,
52                /\swmv="([^"]+)"/,
53                /\sflv="([^"]+)"/,
54                /\swidth="(\d+)"/,
55                /\sheight="(\d+)"/
56            ],
57            config: {
58                selectors: {
59                    quizContainer: '.qsm-quiz-container',
60                    form: '.qsm-quiz-form',
61                    page: '.qsm-page',
62                    pagination: '.qsm-pagination',
63                    previousBtn: '.qsm-previous-btn, .qsm-previous',
64                    nextBtn: '.qsm-next-btn, .qsm-next',
65                    submitBtn: '.qsm-submit-btn',
66                    startBtn: '.qsm-start-btn',
67                    pageCounter: '.qsm-page-counter',
68                    errorMessage: '.qsm-error-message',
69                    modalSubmitBtn: '.submit-the-form',
70                    timerField: '#timer',
71                    timerMsField: 'input[name="timer_ms"]',
72                    currentPageField: 'input[name="current_page"]'
73                },
74                classes: {
75                    hidden: 'qsm-hidden',
76                    disabled: 'qsm-disabled',
77                    loading: 'qsm-loading',
78                    error: 'qsm-error',
79                    current: 'qsm-current-page'
80                },
81                animation: {
82                    duration: 1000,
83                    easing: 'swing'
84                }
85            },
86
87            submit_status: true,
88
89            /**
90             * Get quiz data from either legacy or new variable
91             * Provides backward compatibility
92             */
93            getQuizData: function(quizId) {
94                if (qmn_quiz_data && qmn_quiz_data[quizId]) {
95                    return qmn_quiz_data[quizId];
96                }
97                return {};
98            },
99
100            /**
101             * Initialize navigation for all quizzes on page
102             */
103            init: function(initialPage = 1) {
104                let self = this;
105                
106                // Initialize each quiz form found
107                $(this.config.selectors.quizContainer).each(function() {
108                    
109                    let $quizContainer = $(this);
110                    let quizId = self.getQuizId($quizContainer);
111                    
112                    if (quizId) {
113                        self.initQuiz(quizId, $quizContainer,initialPage);
114                    }
115
116                });
117            },
118
119            initQuizObject: function(quizId, $quizContainer, initialPage) {
120
121                // Get quiz data from backend config
122                let quizData = this.getQuizData(quizId);
123
124                // Find all pages for this quiz
125                let $pages = $quizContainer.find(this.config.selectors.page);
126
127                // Determine if first page is disabled
128                let hasFirstPage = (Number(quizData.disable_first_page) !== 1);
129
130                // Question pages count (excluding intro page)
131                let questionPages = hasFirstPage ? $pages.length - 1 : $pages.length;
132
133                // Build quiz object
134                this.quizObjects[quizId] = {
135                    id: quizId,
136                    quizContainer: $quizContainer,
137                    form: $quizContainer.find(this.config.selectors.form),
138                    pagination: jQuery('.qsm-pagination-' + quizId),
139                    nonceValue: $quizContainer.find('#qsm_nonce_' + quizId).val() 
140                                || $quizContainer.find('input[name="qsm_nonce"]').val(),
141
142                    pages: $pages,
143                    currentPage: initialPage,
144                    totalPages: $pages.length,
145                    questionPages: questionPages,
146                    hasFirstPage: hasFirstPage,
147                    data: quizData,
148
149                    // Validation state
150                    validation: {
151                        enabled: true,
152                        errors: []
153                    },
154
155                    // Runtime values (extendable later)
156                    runtime: {
157                        initialized: false,
158                        started: false,
159                        completed: false,
160                        timeTakenSeconds: 0,
161                    }
162                };
163
164                return this.quizObjects[quizId];
165            },
166
167
168            /**
169             * Initialize a specific quiz
170             */
171            initQuiz: function(quizId, $quizContainer,initialPage) {
172                // Create quiz object
173                let quizObj = this.initQuizObject(quizId, $quizContainer,initialPage);
174
175                // Set start date in localStorage
176                if ( localStorage.getItem('mlw_quiz_start_date' + quizId) == null ) {
177                    localStorage.setItem('mlw_quiz_start_date' + quizId, qmn_ajax_object.start_date);
178                }
179
180                // Initialize pagination UI
181                this.initPagination(quizId);
182
183                // Show first page
184                this.showPage(quizId, quizObj.currentPage);
185
186                this.manageFocus(quizId);
187
188                // Timer
189                let self = this;
190                setInterval(function() {
191                    self.qsmQuizTimeTakenTimer(quizId);
192                }, 1000);
193
194                // Progress Bar
195                if (QSMPagination && QSMPagination.ProgressBar) {
196                    QSMPagination.ProgressBar.initProgressBar(
197                        quizId,
198                        $quizContainer,
199                        quizObj.form
200                    );
201                }
202
203                // Bind events
204                this.bindEvents(quizId);
205
206                this.bindHintToggle(quizObj.quizContainer);
207
208                // Mark as initialized
209                quizObj.runtime.initialized = true;
210
211                // Fire event
212                $(document).trigger('qsm_quiz_initialized', [quizId, quizObj]);
213            },
214
215
216            qsmQuizTimeTakenTimer: function(quizId) {
217                let currentQuiz = this.quizObjects[quizId];
218                if (!currentQuiz) return;
219                
220                let $timerField = currentQuiz.quizContainer.find(this.config.selectors.timerField);
221                if ($timerField.length) {
222                    let timerValue = Number.parseInt($timerField.val()) || 0;
223                    if (Number.isNaN(timerValue)) {
224                        timerValue = 0;
225                    }
226                    timerValue++;
227                    $timerField.val(timerValue);
228                }
229            },
230
231            /**
232             * Initialize pagination for a quiz
233             */
234            initPagination: function(quizId) {
235                let currentQuiz = this.quizObjects[quizId];
236                if (!currentQuiz) return;
237
238                // Hide all pages initially using jQuery hide()
239                currentQuiz.pages.hide();
240                
241                // Initialize navigation buttons and show page counter
242                this.updateNavigationButtons(quizId);
243                this.updatePageCounter(quizId);
244                jQuery(document).trigger('qsm_init_pagination_after', [quizId, qmn_quiz_data]);
245            },
246
247            closeHintWrapper: function($wrapper) {
248                const $toggle = $wrapper.find('.qsm-hint-toggle');
249                const $panel  = $wrapper.find('.qsm-hint-panel').first();
250                const $label  = $toggle.find('.qsm-hint-label');
251                $wrapper.removeClass('is-open');
252                $toggle.attr('aria-expanded', 'false');
253                $panel.attr('aria-hidden', 'true');
254                $label.text($label.data('show') || $label.text());
255                window.setTimeout(function() {
256                    if (!$wrapper.hasClass('is-open')) {
257                        $panel.attr('hidden', 'hidden');
258                    }
259                }, 260);
260            },
261
262            openHintWrapper: function($wrapper) {
263                const $toggle = $wrapper.find('.qsm-hint-toggle');
264                const $panel  = $wrapper.find('.qsm-hint-panel').first();
265                const $label  = $toggle.find('.qsm-hint-label');
266                $panel.removeAttr('hidden');
267                $panel[0]?.getBoundingClientRect();
268                $wrapper.addClass('is-open');
269                $toggle.attr('aria-expanded', 'true');
270                $panel.attr('aria-hidden', 'false');
271                $label.text($label.data('hide') || $label.text());
272            },
273
274            bindHintToggle: function($quizContainer) {
275                if (!$quizContainer?.length) {
276                    return;
277                }
278                $quizContainer.off('click.qsmHint')
279                    .on('click.qsmHint', '.qsm-hint-toggle', (e) => {
280                        e.preventDefault();
281                        const $toggle  = $(e.currentTarget);
282                        const $wrapper = $toggle.closest('.qsm-hint-wrapper');
283                        const isOpen   = $wrapper.hasClass('is-open');
284
285                        $quizContainer.find('.qsm-hint-wrapper.is-open').not($wrapper).each((_, el) => {
286                            this.closeHintWrapper($(el));
287                        });
288
289                        if (isOpen) {
290                            this.closeHintWrapper($wrapper);
291                        } else {
292                            this.openHintWrapper($wrapper);
293                        }
294                    });
295            },
296
297            /**
298             * Initialize validation
299             */
300            initValidation: function(quizId) {
301                let currentQuiz = this.quizObjects[quizId];
302                if (!currentQuiz) return;
303
304                // Set up validation rules based on quiz configuration
305                currentQuiz.validation.rules = this.getValidationRules(quizId);
306            },
307
308            /**
309             * Bind events for a quiz instance
310             */
311            bindEvents: function(quizId) {
312                let self = this;
313                let currentQuiz = this.quizObjects[quizId];
314                let $form = currentQuiz.form;
315                let $start_btn = '.qsm-start-btn-'+quizId;
316                let $prev_btn = '.qsm-previous-btn-'+quizId;
317                let $next_btn = '.qsm-next-btn-'+quizId;
318                let $submit_btn = '.qsm-submit-btn-'+quizId;
319                
320                // =========================================
321                // 🔥 Prevent duplicate click handlers
322                // =========================================
323                $(document).off('click', $start_btn);
324                $(document).off('click', $prev_btn);
325                $(document).off('click', $next_btn);
326                $(document).off('click', $submit_btn);
327                
328                // Navigation button clicks - bind to container since navigation is outside form
329                $(document).on('click', $prev_btn, function(e) {
330                    e.preventDefault();
331                    self.previousPage(quizId);
332                });
333
334                $(document).on('click', $next_btn, function(e) {
335                    e.preventDefault();
336                    self.nextPage(quizId);
337                });
338
339                // Start button click - bind to container (multiple selectors for compatibility)
340                $(document).on('click', $start_btn, function(e) {
341                    e.preventDefault();
342                    // Validate current page before proceeding
343                    if (!self.validateCurrentPage(quizId)) {
344                        return;
345                    }
346                    self.startQuiz(quizId);
347                    self.nextPage(quizId);
348                });
349
350                // Submit button click - bind to container since submit button is in navigation
351                $(document).on('click', $submit_btn, function(e) {
352                    e.preventDefault();
353                    jQuery(document).trigger('qsm_quiz_submit_trigger', [quizId]);
354
355                    self.submitFormWithRecaptcha(quizId);
356                    return false;
357                });
358                
359                $(document).on('click', this.config.selectors.modalSubmitBtn, function(e) {
360                    e.preventDefault();
361                    jQuery(document).trigger('qsm_quiz_submit_trigger', [quizId]);
362
363                    self.submitFormWithRecaptcha(quizId);
364                    return false;
365                });
366
367                // Form submission
368                $form.on('submit', function(e) {
369                    e.preventDefault();
370                    jQuery(document).trigger('qsm_quiz_submit_trigger', [quizId]);
371
372                    self.submitFormWithRecaptcha(quizId);
373                    return false;
374                });
375
376                // Input change events for real-time validation
377                $form.on('change input', 'input, select, textarea', function() {
378                    self.clearFieldError($(this));
379                });
380
381                // Keyboard navigation
382                $form.on('keydown', function(e) {
383                    self.handleKeyboardNavigation(e, quizId);
384                });
385                
386                // Answer selection event handlers
387                self.bindAnswerEvents(quizId);
388            },
389
390            submitFormWithRecaptcha: function(quizId) {
391                let quizData = this.quizObjects[quizId];
392                if (!quizData) return;
393
394                let $form = quizData.form;
395                let $recaptcha = $form.find('#qsm_grecaptcha_v3');
396
397                if (!$recaptcha.length) {
398                    this.submitForm(quizId);
399                    return;
400                }
401
402                let siteKey = $form.find('#qsm_grecaptcha_v3_sitekey').val();
403                let submitAction = $form.find('#qsm_grecaptcha_v3_nonce').val();
404
405                if (!siteKey || typeof window.grecaptcha === 'undefined' || !window.grecaptcha || typeof window.grecaptcha.execute !== 'function') {
406                    this.submitForm(quizId);
407                    return;
408                }
409
410                window.grecaptcha.ready(function () {
411                    window.grecaptcha.execute(siteKey, { action: submitAction }).then(function (token) {
412                        $form.find('#qsm_grecaptcha_v3_response').val(token);
413                        this.submitForm(quizId);
414                    });
415                });
416            },
417            
418            /**
419             * Bind answer selection event handlers (matching legacy qsm-quiz.js)
420             */
421            bindAnswerEvents: function(quizId) {
422                let self = this;
423                let currentQuiz = this.quizObjects[quizId];
424                let $container = currentQuiz.quizContainer;
425
426                // bindAnswerEvents() runs again on every lazy-loaded page navigation.
427                // The handlers below are delegated on the persistent $container, so without
428                // removing the previous bindings they stack up and fire qsm_after_select_answer
429                // once per navigation (1x on Q1, 2x on Q2, ...). Namespace + off prevents that.
430                $container.off('.qsmAnswerEvents');
431
432                // Remove active-question highlight when using mouse (border is for keyboard navigation only)
433                $container.off('click.qsmActiveQuestion');
434                $container.on('click.qsmActiveQuestion', '.qsm-question-wrapper', function() {
435                    $container.find('.qsm-question-wrapper').removeClass('qsm-active-question qsm-last-active-question');
436                });
437
438                // Multiple choice radio buttons and dropdowns (matching legacy: .qmn-multiple-choice-input, .qsm_dropdown, .mlw_answer_date)
439                $container.on('change.qsmAnswerEvents', '.qmn-multiple-choice-input, .qsm_dropdown, .mlw_answer_date', function(e) {
440                    let $i_this = $(this);
441                    let value = $i_this.val();
442                    let $this = $i_this.closest('.quiz_section, .qsm-question');
443                    let question_id = $i_this.attr('name').split('question')[1];
444                    let inputType;
445                    
446                    if ($i_this.hasClass('mlw_answer_date')) {
447                        inputType = 'input';
448                    } else {
449                        inputType = 'radio';
450                    }
451                    
452                    let quizData = self.getQuizData(quizId);
453                    
454                    // Quick result / inline feedback
455                    if (Number(quizData.enable_quick_result_mc) === 1) {
456                        self.qsmShowInlineResult(quizId, question_id, value, $this, inputType, $i_this);
457                    } else if (Number(quizData.enable_quick_correct_answer_info) !== 0) {
458                        let data = self.qsmQuestionQuickResultJs(question_id, value, inputType, quizData.enable_quick_correct_answer_info, quizId);
459                        $this.find('.quick-question-res-p, .qsm-inline-correct-info').remove();
460                        if (value && value.length > 0 && data.success !== '') {
461                            $this.append('<div class="qsm-inline-correct-info">' + self.qsmCheckShortcode(data.message) + '</div>');
462                        }
463                    }
464                    
465                    // Trigger after select answer event (for addons)
466                    $(document).trigger('qsm_after_select_answer', [quizId, question_id, value, $this, inputType]);
467
468                    // Auto-submit if answer is wrong
469                    if (quizData.end_quiz_if_wrong > 0 && !$container.find('.qsm-next-btn:visible').length) {
470                        self.qsmSubmitQuizIfAnswerWrong(quizId, question_id, value, $this, currentQuiz.form);
471                    }
472                });
473                
474                // Multiple response checkboxes (matching legacy: .qsm-multiple-response-input)
475                $container.on('change.qsmAnswerEvents', '.qsm-multiple-response-input', function(e) {
476                    let $i_this = $(this);
477                    let question_id = $i_this.attr('name').split('question')[1];
478                    let $this = $i_this.closest('.quiz_section, .qsm-question');
479                    let parent = $i_this.closest('.qmn_check_answers, .qsm-answer-options');
480                    let checkedValues = parent.find('input[type="checkbox"]:checked').map(function() {
481                        return $(this).val();
482                    }).get();
483                    
484                    let quizData = self.getQuizData(quizId);
485
486                    // Auto-submit if answer is wrong
487                    if (quizData.end_quiz_if_wrong > 0 && !$container.find('.qsm-next-btn:visible').length) {
488                        self.qsmSubmitQuizIfAnswerWrong(quizId, question_id, checkedValues, $this, currentQuiz.form, 'checkbox');
489                    }
490                    
491                    // Quick result / inline feedback
492                    if (Number(quizData.enable_quick_result_mc) === 1) {
493                        self.qsmShowInlineResult(quizId, question_id, checkedValues, $this, 'checkbox', $i_this);
494                    } else if (Number(quizData.enable_quick_correct_answer_info) !== 0) {
495                        let data = self.qsmQuestionQuickResultJs(question_id, checkedValues, 'checkbox', quizData.enable_quick_correct_answer_info, quizId);
496                        $this.find('.quick-question-res-p, .qsm-inline-correct-info').remove();
497                        if (checkedValues.length > 0 && data.success != '') {
498                            $this.append('<div class="qsm-inline-correct-info">' + self.qsmCheckShortcode(data.message) + '</div>');
499                        }
500                    }
501                    
502                    // Trigger after select answer event (for addons)
503                    $(document).trigger('qsm_after_select_answer', [quizId, question_id, checkedValues, $this, 'checkbox']);
504                });
505                
506                // File upload functionality (matching legacy: .mlw_answer_file_upload)
507                this.bindFileUploadEvents(quizId);
508                
509                // Text, number, and fill-blank inputs (matching legacy: .mlw_answer_open_text, .mlw_answer_number, .qmn_fill_blank)
510                let qsm_inline_result_timer;
511                $container.on('keyup.qsmAnswerEvents', '.mlw_answer_open_text, .mlw_answer_number, .qmn_fill_blank', function(e) {
512                    let $i_this = $(this);
513                    let question_id = $i_this.attr('name').split('question')[1];
514                    let $this = $i_this.closest('.quiz_section, .qsm-question');
515                    let value;
516                    
517                    if ($i_this.hasClass('qmn_fill_blank')) {
518                        value = $this.find('.qmn_fill_blank').map(function() {
519                            let val = $(this).val();
520                            return val || null;
521                        }).get().filter(function(v) { return v !== null; });
522                    } else {
523                        value = $i_this.val();
524                    }
525                    
526                    let sendValue;
527                    if (typeof value === 'string') {
528                        sendValue = value.trim();
529                    } else if (value.length) {
530                        sendValue = value[value.length - 1];
531                    } else {
532                        sendValue = '';
533                    }
534                    
535                    let quizData = self.getQuizData(quizId);
536                    
537                    clearTimeout(qsm_inline_result_timer);
538                    qsm_inline_result_timer = setTimeout(function() {
539                        let showFeedback = true;
540                        
541                        // For fill-blank, only show feedback when all blanks are filled
542                        if ($i_this.hasClass('qmn_fill_blank')) {
543                            let $allBlanks = $this.find('.qmn_fill_blank');
544                            let totalBlanks = $allBlanks.length;
545                            let filledBlanks = $allBlanks.filter(function() {
546                                return $(this).val().trim() !== '';
547                            }).length;
548                            showFeedback = (totalBlanks > 0 && filledBlanks === totalBlanks);
549                            
550                            if (!showFeedback) {
551                                $this.find('.quick-question-res-p, .qsm-inline-correct-info').remove();
552                            }
553                        }
554                        
555                        if (showFeedback) {
556                            if (Number(quizData.enable_quick_result_mc) === 1) {
557                                self.qsmShowInlineResult(quizId, question_id, sendValue, $this, 'input', $i_this, $this.find('.qmn_fill_blank').index($i_this));
558                            } else if (Number(quizData.enable_quick_correct_answer_info) !== 0) {
559                                let data = self.qsmQuestionQuickResultJs(question_id, sendValue, 'input', quizData.enable_quick_correct_answer_info, quizId, $this.find('.qmn_fill_blank').index($i_this));
560                                $this.find('.quick-question-res-p, .qsm-inline-correct-info').remove();
561                                if (value.length > 0 && data.success != '') {
562                                    $this.append('<div class="qsm-inline-correct-info">' + self.qsmCheckShortcode(data.message) + '</div>');
563                                }
564                            }
565                        }
566                        
567                        // Trigger after select answer event (for addons)
568                        $(document).trigger('qsm_after_select_answer', [quizId, question_id, value, $this, 'input', $this.find('.qmn_fill_blank').index($i_this)]);
569                    }, 2000);
570                });
571            },
572            
573            /**
574             * Bind file upload event handlers (matching legacy file upload functionality)
575             */
576            bindFileUploadEvents: function(quizId) {
577                let self = this;
578                let currentQuiz = this.quizObjects[quizId];
579                let $container = currentQuiz.quizContainer;
580                $container.off('.qsmFileUpload');
581
582                // File input change event - validation and upload
583                $container.on('change.qsmFileUpload', '.quiz_section .mlw_answer_file_upload', async function() {
584                    let $this = $(this);
585                    let file_data = $this.prop('files')[0];
586                    if (!file_data) {
587                        await self.qsmRemoveUploadedFile($this.parent('.quiz_section, .qsm-question').find('.qsm-file-upload-container').find('.remove-uploaded-file'));
588                        return false;
589                    }
590                    
591                    let question_id = $this.parent('.quiz_section').find('.mlw_answer_file_upload').attr("name").replace('qsm_file_question', '');
592                    let quizData = self.getQuizData(quizId);
593                    let file_upload_type = quizData.questions_settings && quizData.questions_settings[question_id] ? quizData.questions_settings[question_id].file_upload_type : '';
594                    let file_upload_limit = quizData.questions_settings && quizData.questions_settings[question_id] ? quizData.questions_settings[question_id].file_upload_limit : 1;
595                    file_upload_limit = file_upload_limit || 1; // Default 1MB
596                    
597                    let $file_upload_status = $this.parent('.quiz_section').find('.qsm-file-upload-status');
598                    $file_upload_status.removeClass('qsm-error qsm-success qsm-processing');
599                    $file_upload_status.addClass('qsm-processing');
600                    
601                    // Get processing message
602                    let processingMsg = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.validate_process) ? qmn_ajax_object.validate_process : 'Processing...';
603                    $file_upload_status.text(processingMsg).show();
604                    
605                    // Build allowed mime types array
606                    let allowed_mime_types = [];
607                    if (file_upload_type) {
608                        let types = file_upload_type.split(',');
609                        types.forEach(function(type) {
610                            type = type.trim();
611                            if (type === 'image') {
612                                allowed_mime_types.push('image/jpeg', 'image/png', 'image/x-icon', 'image/gif', 'image/webp');
613                            } else if (type === 'doc') {
614                                allowed_mime_types.push('application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
615                            } else if (type === 'excel') {
616                                allowed_mime_types.push('application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', 
617                                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv');
618                            } else if (type) {
619                                allowed_mime_types.push(type);
620                            }
621                        });
622                    }
623                    
624                    let trigger_message, trigger_type = '';
625                    
626                    // Validate file type and size
627                    if (!allowed_mime_types.includes(file_data.type) || (file_upload_limit > 0 && file_data.size > file_upload_limit * 1024 * 1024)) {
628                        let warning_message = '';
629                        if (file_upload_limit > 0 && file_data.size > file_upload_limit * 1024 * 1024) {
630                            warning_message = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.invalid_file_size) ? 
631                                            qmn_ajax_object.invalid_file_size + file_upload_limit + 'MB' : 
632                                            'File size must not exceed ' + file_upload_limit + 'MB';
633                        } else {
634                            warning_message = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.invalid_file_type) ? 
635                                            qmn_ajax_object.invalid_file_type + file_upload_type : 
636                                            'Invalid file type. Allowed: ' + file_upload_type;
637                        }
638                        $this.val('');
639                        $file_upload_status.removeClass('qsm-processing');
640                        $file_upload_status.addClass('qsm-error').text(warning_message).show();
641                        trigger_message = warning_message;
642                        trigger_type = 'error';
643                    } else {
644                        // Validation passed - update UI to show success
645                        $file_upload_status.removeClass('qsm-error qsm-processing');
646                        $file_upload_status.addClass('qsm-success');
647                        $this.parent('.quiz_section').find('.qsm-file-upload-name').html($this[0].files[0].name).show();
648                        $this.parent('.quiz_section').find('.qsm-file-upload-container').find('.remove-uploaded-file').show();
649                        
650                        let successMsg = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.validate_success) ? qmn_ajax_object.validate_success : 'File uploaded successfully';
651                        $file_upload_status.text(successMsg).show();
652                        trigger_message = successMsg;
653                        trigger_type = 'success';
654                    }
655                    
656                    // Trigger after file upload event
657                    const obj = {
658                        type: trigger_type,
659                        message: trigger_message,
660                        file_name: file_data.name,
661                        file_type: file_data.type,
662                        file_size: file_data.size,
663                        file_path: '',
664                        file_url: '',
665                    };
666                    $(document).trigger('qsm_after_file_upload', [$this.parent(), obj]);
667                    
668                    return false;
669                });
670                
671                // Remove file click event
672                $container.on('click.qsmFileUpload', '.quiz_section .remove-uploaded-file', async function() {
673                    await self.qsmRemoveUploadedFile($(this));
674                    return false;
675                });
676                
677                // Click on upload container to trigger file input
678                $container.on('click.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function(e) {
679                    e.preventDefault();
680                    // Don't trigger file upload if clicking on remove button
681                    if (!$(e.target).hasClass('remove-uploaded-file')) {
682                        $(this).prev('.mlw_answer_file_upload').trigger('click');
683                    }
684                });
685                
686                // Drag and drop events
687                $container.on('dragover.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function(e) {
688                    e.preventDefault();
689                    e.stopPropagation();
690                    $(this).addClass('file-hover');
691                });
692                
693                $container.on('dragenter.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function(e) {
694                    e.preventDefault();
695                    e.stopPropagation();
696                });
697                
698                $container.on('dragleave.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function(e) {
699                    e.preventDefault();
700                    e.stopPropagation();
701                    $(this).removeClass('file-hover');
702                });
703                
704                $container.on('drop.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function(e) {
705                    $(this).removeClass('file-hover');
706                    if (e.originalEvent.dataTransfer) {
707                        if (e.originalEvent.dataTransfer.files.length) {
708                            e.preventDefault();
709                            e.stopPropagation();
710                            $(this).find('.qsm-file-upload-name').html(e.originalEvent.dataTransfer.files[0].name).fadeIn();
711                            $(this).prev('.mlw_answer_file_upload').prop('files', e.originalEvent.dataTransfer.files);
712                            $(this).prev('.mlw_answer_file_upload').trigger('change');
713                        }
714                    }
715                });
716                
717                $container.on('mouseleave.qsmFileUpload', '.quiz_section .qsm-file-upload-container', function() {
718                    $(this).removeClass('file-hover');
719                });
720
721                //Deselect all answer on select
722                jQuery('.qsm-deselect-answer').click(function (e) {
723                    e.preventDefault();
724                    jQuery(this).parents('.quiz_section').find('input[type="radio"]').prop('checked', false);
725                    jQuery(this).parents('.quiz_section').find('input[type="radio"]:hidden').prop('checked', true);
726                });
727
728                //Submit the form on popup click
729                jQuery(document).on('click', '.submit-the-form', function (e) {
730                    e.preventDefault();
731                    // Triggger the click event on the quiz form's submit button.
732                    jQuery('.qsm-submit-btn').trigger('click');
733                    jQuery('#modal-3').removeClass('is-open');
734                });
735
736                jQuery('.pagetime-goto-nextpage').click(function (e) {
737                    e.preventDefault();
738                    let quiz_id = jQuery(this).data('quiz_id');
739                    let $container = jQuery('#quizForm' + quiz_id).closest('.qmn_quiz_container');
740                    if(!$container.find('.qsm-submit-btn').is(':visible')) {
741                        QSMPagination.Navigation.nextPage(quiz_id);
742                        QSMPagination.Navigation.scrollToQuiz(quiz_id);
743                    }else{
744                        $container.find(".mlw_custom_next").hide();
745                    }
746                });
747
748                jQuery(document).on('keyup', '.mlwPhoneNumber', function (e) {
749                    this.value = this.value.replace(/[^- +()0-9\.]/g, '');
750                });
751
752                jQuery(document).on('click', '.qsm_social_share_link', function (e) {
753                    e.preventDefault();
754                    let network = jQuery(this).attr('data-network');
755                    let share_url = jQuery(this).attr('data-link');
756                    let social_text = jQuery(this).attr('data-text');
757                    let social_id = jQuery(this).attr('data-id');
758                    let url = '';
759                    if (network == 'facebook') {
760                        url = "https://www.facebook.com/dialog/feed?" + "display=popup&" + "app_id=" + social_id +
761                            "&" + "link=" + encodeURIComponent(share_url) + "&" + "name=" + social_text;
762                    }
763                    if (network == 'twitter') {
764                        url = "https://twitter.com/intent/tweet?text=" + social_text;
765                    }
766                    if (network == 'linkedin') {
767                        url = "https://www.linkedin.com/feed/?text=" + social_text;
768                    }
769                    let sTop = window.screen.height / 2 - (218);
770                    let sLeft = window.screen.width / 2 - (313);
771                    let sqShareOptions = "height=400,width=580,toolbar=0,status=0,location=0,menubar=0,directories=0,scrollbars=0,top=" + sTop + ",left=" + sLeft;
772                    window.open(url, "Share", sqShareOptions);
773                    return false;
774                });
775            },
776            
777            /**
778             * Remove uploaded file (matching legacy qsm_remove_uploaded_file_fd_question)
779             */
780            qsmRemoveUploadedFile: async function($removeButton) {
781                let parents_section = $removeButton.parents('.quiz_section, .qsm-question');
782                parents_section.find('.qsm-file-upload-status').removeClass('qsm-processing qsm-success');
783                parents_section.find('.qsm-file-upload-status').addClass('qsm-error');
784                
785                let removeMsg = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.remove_file) ? qmn_ajax_object.remove_file : 'Removing file...';
786                parents_section.find('.qsm-file-upload-status').html(removeMsg).show();
787                parents_section.find('.qsm-file-upload-name').html('').show();
788                $removeButton.hide();
789                parents_section.find('.mlw_answer_file_upload').val('');
790                
791                let successMsg = (typeof qmn_ajax_object !== 'undefined' && qmn_ajax_object.remove_file_success) ? qmn_ajax_object.remove_file_success : 'File removed successfully';
792                parents_section.find('.qsm-file-upload-status').text(successMsg);
793                
794                // Trigger after file remove event
795                $(document).trigger('qsm_after_file_remove', [$removeButton.parent(), {type: 'success', message: successMsg}]);
796            },
797
798            /**
799             * Start the quiz (handle start button click)
800             */
801            startQuiz: function(quizId) {
802                let quizData = this.quizObjects[quizId];
803                if (!quizData) return;
804                
805                // Start timer if available
806                if (QSMPagination && QSMPagination.Timer) {
807                    QSMPagination.Timer.start(quizId);
808                }
809                
810                // Trigger quiz started event
811                $(document).trigger('qsm_quiz_started', [quizId, quizData]);
812            },
813
814            /**
815             * Navigate to specific page (1-based indexing)
816             */
817            goToPage: function(quizId, pageNumber, updateHistory) {
818                let self = this;
819                let quizData = self.quizObjects[quizId];
820                if (!quizData || pageNumber < 1 || pageNumber > quizData.totalPages) {
821                    return;
822                }
823                jQuery(document).trigger('qsm_go_to_page_before', [quizId, pageNumber]);
824
825                jQuery('.qsm-multiple-response-input:checked, .qmn-multiple-choice-input:checked , .qsm_select:visible').each(function () {
826                    if (quizData.data.end_quiz_if_wrong > 0 && jQuery(this).parents().is(':visible') && jQuery(this).is('input, select')) {
827                        if (jQuery(this).parents('.qmn_radio_answers, .qsm_check_answer')) {
828                            let question_id = jQuery(this).attr('name').split('question')[1],
829                            value = jQuery(this).val(),
830                            $this = jQuery(this).parents('.quiz_section');
831                            if (value != "" && value != null) {
832                                self.qsmSubmitQuizIfAnswerWrong(quizId, question_id, value, $this, quizData.form);
833                            }
834                        }
835                    }
836                })
837
838                updateHistory = updateHistory !== false; // Default to true
839
840                // Update current page
841                quizData.currentPage = pageNumber;
842                
843                // Show the target page
844                this.showPage(quizId, pageNumber);
845                
846                // Update UI components
847                this.updateNavigationButtons(quizId);
848                this.updatePageCounter(quizId);
849
850                // Scroll to top of quiz
851                if (quizData.data.disable_scroll_next_previous_click != 1) {
852                    this.scrollToQuiz(quizId);
853                }
854
855                // Trigger after page change event
856                $(document).trigger('qsm_go_to_page_after', [quizId, pageNumber, quizData]);
857            },
858            
859            /**
860             * Show specific page (helper method)
861             */
862            showPage: function(quizId, pageNumber) {
863                let quizData = this.quizObjects[quizId];
864                if (!quizData) return;
865                
866                // Trigger before page show event
867                $(document).trigger('qsm_show_page_before', [quizId, pageNumber, quizData]);
868                
869                // Hide all pages
870                quizData.pages.hide();
871                
872                // Show target page (convert to 0-based index for DOM)
873                let $targetPage = quizData.pages.eq(pageNumber - 1);
874                // Trigger page timer init/update (used by Advanced Timer addon)
875                jQuery(document).trigger('end_page_timer_init_page_timer', [quizId, $targetPage]);
876                if ($targetPage.length > 0) {
877                    $targetPage.show();
878                    // Check if this page needs lazy loading
879                    if ($targetPage.hasClass('qsm-lazy-load-page') && $targetPage.attr('data-lazy-load') === '1') {
880                        this.loadPageQuestions(quizId, $targetPage, pageNumber);
881                    }
882                    
883                    // Preemptively load next pages (load 2 pages ahead)
884                    this.preloadNextPages(quizId, pageNumber);
885                }
886                
887                // Trigger after page show event
888                $(document).trigger('qsm_show_page_after', [quizId, pageNumber, quizData]);
889            },
890            
891            /**
892             * Preload next pages (load 2 pages ahead of current page)
893             * This ensures smooth navigation by loading pages before user reaches them
894             */
895            preloadNextPages: function(quizId, currentPage) {
896                let self = this;
897                let quizData = this.quizObjects[quizId];
898                if (!quizData) return;
899                
900                // Calculate which pages to preload (next 2 pages)
901                let pagesToPreload = [];
902                
903                // Preload page currentPage + 1 and currentPage + 2
904                for (let i = 1; i <= 2; i++) {
905                    let nextPageNum = currentPage + i;
906                    if (nextPageNum <= quizData.totalPages) {
907                        pagesToPreload.push(nextPageNum);
908                    }
909                }
910                
911                // Load each page that needs preloading
912                pagesToPreload.forEach(function(pageNum) {
913                    let $nextPage = quizData.pages.eq(pageNum - 1);
914                    
915                    // Only preload if:
916                    // 1. Page exists
917                    // 2. Page is marked for lazy loading
918                    // 3. Page hasn't been loaded yet
919                    // 4. Page is not currently being loaded
920                    if ($nextPage.length > 0 && 
921                        $nextPage.hasClass('qsm-lazy-load-page') && 
922                        $nextPage.attr('data-lazy-load') === '1' &&
923                        !$nextPage.hasClass('qsm-loading')) {
924                        
925                        self.loadPageQuestions(quizId, $nextPage, pageNum);
926                    }
927                });
928            },
929            
930            /**
931             * Load questions for a lazy-loaded page via AJAX
932             */
933            loadPageQuestions: function(quizId, $page, pageNumber) {
934                let self = this;
935                let quizData = this.quizObjects[quizId];
936                if (!quizData) return;
937                
938                // Check if already loading or loaded
939                if ($page.hasClass('qsm-loading') || $page.hasClass('qsm-loaded-page')) {
940                    return;
941                }
942                
943                // Mark as loading
944                $page.addClass('qsm-loading');
945                
946                // Show loading spinner
947                $page.find('.qsm-lazy-load-spinner').show();
948                
949                // Get question IDs and other data from page attributes
950                let questionIds = $page.attr('data-question-ids') || '';
951                let questionStartNumber = Number.parseInt($page.attr('data-question-start-number')) || 1;
952
953                // Prepare AJAX data
954                let ajaxData = {
955                    action: 'qsm_load_page_questions',
956                    nonce: quizData.data.lazy_load_nonce || quizData.data.nonce || (typeof qmn_ajax_object !== 'undefined' ? qmn_ajax_object.security : ''),
957                    quiz_id: quizId,
958                    page_number: pageNumber,
959                    question_ids: questionIds,
960                    question_start_number: questionStartNumber,
961                    randomness_order: JSON.stringify(quizData.data.randomness_order || [])
962                };
963                
964                // Trigger before lazy load event
965                $(document).trigger('qsm_before_lazy_load', [quizId, pageNumber, $page]);
966                
967                // Make AJAX request
968                $.ajax({
969                    url: quizData.data.ajax_url || (typeof qmn_ajax_object !== 'undefined' ? qmn_ajax_object.ajaxurl : '/wp-admin/admin-ajax.php'),
970                    type: 'POST',
971                    data: ajaxData,
972                    success: function(response) {
973                        if (response.success && response.data.html) {
974                            // Insert questions HTML where the placeholder sits, then remove it.
975                            // The placeholder is the stable anchor: .pages_count may be absent
976                            // when the "show pagination count" option is disabled.
977                            let $placeholder = $page.find('.qsm-lazy-load-placeholder');
978                            if ($placeholder.length) {
979                                $placeholder.before(response.data.html);
980                                $placeholder.remove();
981                            } else if ($page.find('.pages_count').length) {
982                                $page.find('.pages_count').before(response.data.html);
983                            } else {
984                                $page.append(response.data.html);
985                            }
986
987                            // Mark page as loaded
988                            $page.removeClass('qsm-lazy-load-page qsm-loading');
989                            $page.addClass('qsm-loaded-page');
990                            $page.attr('data-lazy-load', '0');
991
992                            // Persist answer randomization order from the AJAX response
993                            // so the server can render results in the order the user saw.
994                            if (response.data.quiz_answer_random_ids) {
995                                self.appendLazyAnswerRandomIds(quizId, response.data.quiz_answer_random_ids);
996                            }
997
998                            // Re-bind events for newly loaded questions
999                            self.bindAnswerEvents(quizId);
1000
1001                            // Initialize any new file upload fields
1002                            self.bindFileUploadEvents(quizId);
1003
1004                            // Trigger after lazy load event
1005                            $(document).trigger('qsm_after_lazy_load', [quizId, pageNumber, $page, response.data]);
1006                        } else {
1007                            self.handleLazyLoadError(quizId, $page, response.data ? response.data.message : 'Unknown error');
1008                        }
1009                    },
1010                    error: function(xhr, status, error) {
1011                        self.handleLazyLoadError(quizId, $page, 'AJAX error: ' + error);
1012                    }
1013                });
1014            },
1015            
1016            /**
1017             * Inject hidden inputs that carry the shuffled answer key order for
1018             * lazy-loaded questions, so result rendering can match the order the
1019             * user actually saw.
1020             */
1021            appendLazyAnswerRandomIds: function(quizId, randomIdsMap) {
1022                if (!randomIdsMap || typeof randomIdsMap !== 'object') {
1023                    return;
1024                }
1025                let $form = $('#quizForm' + quizId);
1026                if (!$form.length) {
1027                    $form = $('form.qmn_quiz_container[data-quiz-id="' + quizId + '"]');
1028                }
1029                if (!$form.length) {
1030                    return;
1031                }
1032                Object.keys(randomIdsMap).forEach(function(questionId) {
1033                    let keys = randomIdsMap[questionId];
1034                    if (!Array.isArray(keys)) {
1035                        return;
1036                    }
1037                    $form.find('input[data-qsm-lazy-answer-qid="' + questionId + '"]').remove();
1038                    keys.forEach(function(key) {
1039                        $('<input>', {
1040                            type: 'hidden',
1041                            name: 'qsm_lazy_answer_random_ids[' + questionId + '][]',
1042                            value: key,
1043                            'data-qsm-lazy-answer-qid': questionId
1044                        }).appendTo($form);
1045                    });
1046                });
1047            },
1048
1049            /**
1050             * Handle lazy load errors
1051             */
1052            handleLazyLoadError: function(quizId, $page, errorMessage) {
1053                
1054                // Hide spinner
1055                $page.find('.qsm-lazy-load-spinner').hide();
1056                
1057                // Show error message
1058                let errorHtml = '<div class="qsm-error-message" style="padding: 20px; color: #d63638;">' +
1059                    '<strong>Error loading questions:</strong><br>' + errorMessage +
1060                    '<br><button class="qsm-retry-load" style="margin-top: 10px;">Retry</button>' +
1061                    '</div>';
1062                $page.find('.qsm-lazy-load-placeholder').html(errorHtml);
1063                
1064                // Remove loading class
1065                $page.removeClass('qsm-loading');
1066                
1067                // Bind retry button
1068                let self = this;
1069                $page.find('.qsm-retry-load').on('click', function() {
1070                    $page.find('.qsm-error-message').remove();
1071                    $page.find('.qsm-lazy-load-placeholder').html('<div class="qsm-lazy-load-spinner" style="display: none;"></div>');
1072                    self.loadPageQuestions(quizId, $page, Number.parseInt($page.attr('data-page')));
1073                });
1074                
1075                // Trigger error event
1076                $(document).trigger('qsm_lazy_load_error', [quizId, $page, errorMessage]);
1077            },
1078
1079            /**
1080             * Go to next page
1081             */
1082            nextPage: function(quizId) {
1083                let quizData = this.quizObjects[quizId];
1084
1085                if (!quizData) return;
1086
1087                // Validate current page before proceeding
1088                if (!this.validateCurrentPage(quizId)) {
1089                    return;
1090                }
1091
1092                // Check if this is the last page
1093                if (quizData.currentPage >= quizData.totalPages) {
1094                    // Submit form if on last page
1095                    quizData.form.trigger('submit');
1096                    return;
1097                }
1098
1099                // Go to next page
1100                this.goToPage(quizId, quizData.currentPage + 1);
1101
1102                // Manage focus for accessibility
1103                this.manageFocus(quizId);
1104                
1105                // Trigger next button click event (for legacy compatibility)
1106                $(document).trigger('qsm_next_button_click_after', [quizId]);
1107            },
1108
1109            /**
1110             * Go to previous page
1111             */
1112            previousPage: function(quizId) {
1113                let quizData = this.quizObjects[quizId];
1114                if (!quizData) return;
1115                
1116                // Check minimum page based on first page setting
1117                let minPage;
1118                if (quizData.hasFirstPage) {
1119                    // First page enabled: Can't go back past page 2 (first question page, not welcome page)
1120                    minPage = 2;
1121                } else {
1122                    // No first page, so all pages are question pages
1123                    minPage = 1;
1124                }
1125                
1126                if (quizData.currentPage < minPage) return;
1127                this.goToPage(quizId, quizData.currentPage - 1);
1128                
1129                // Manage focus for accessibility
1130                this.manageFocus(quizId);
1131                
1132                // Trigger previous button click event (for legacy compatibility)
1133                $(document).trigger('qsm_previous_button_click_after', [quizId]);
1134            },
1135
1136            /**
1137             * Update navigation buttons visibility and state
1138             */
1139            updateNavigationButtons: function(quizId) {
1140                let quizData = this.quizObjects[quizId];
1141                if (!quizData) return;
1142
1143                let $previousBtn = jQuery('.qsm-previous-btn-'+quizId);
1144                let $nextBtn = jQuery('.qsm-next-btn-'+quizId);
1145                let $submitBtn = jQuery('.qsm-submit-btn-'+quizId);
1146                let $startBtn = jQuery('.qsm-start-btn-'+quizId);
1147                
1148                let currentPage = quizData.currentPage;
1149                let isFirstPage = (currentPage == 1);
1150                let isLastPage = (currentPage == quizData.totalPages);
1151                let showStartButton = (isFirstPage && quizData.hasFirstPage);
1152                
1153                let $featureImage = quizData.quizContainer.find('.qsm-quiz-default-feature-image');
1154                if (showStartButton) {
1155                    $featureImage.show();
1156                } else {
1157                    $featureImage.hide();
1158                }
1159
1160                // Simple button visibility logic
1161                if (showStartButton) {
1162                    // First page (welcome page) with start button
1163                    $startBtn.show();
1164                    $previousBtn.hide();
1165                    $nextBtn.hide();
1166                    $submitBtn.hide();
1167                } else {
1168                    // Regular navigation pages (question pages)
1169                    $startBtn.hide();
1170
1171                    // Previous button logic:
1172                    // - Hide on first page when no welcome page
1173                    // - Hide on page 2 when there IS a welcome page (can't go back from first question page to welcome)
1174                    // - Show otherwise
1175                    if (currentPage < 2) {
1176                        $previousBtn.hide();
1177                    } else {
1178                        $previousBtn.show();
1179                    }
1180
1181                    // Next/Submit buttons: submit on last page, next otherwise
1182                    if (isLastPage) {
1183                        $nextBtn.hide();
1184                        $submitBtn.show();
1185                        
1186                    } else {
1187                        $nextBtn.show();
1188                        $submitBtn.hide();
1189                        
1190                    }
1191                }
1192            },
1193
1194            /**
1195             * Update page counter display
1196             */
1197            updatePageCounter: function(quizId) {
1198                let quizData = this.quizObjects[quizId];
1199                if (!quizData) return;
1200                
1201                let currentPage = quizData.form.find(this.config.selectors.currentPageField);
1202                currentPage.val(quizData.currentPage);
1203
1204                let $counter = quizData.quizContainer.find(this.config.selectors.pageCounter);
1205                if ($counter.length) {
1206                    // Don't show page counter for start page (welcome page)
1207                    if (quizData.currentPage === 1 && quizData.hasFirstPage) {
1208                        $counter.hide();
1209                    } else {
1210                        // Calculate display numbers for question pages only
1211                        let displayPage, displayTotal;
1212                        
1213                        if (quizData.hasFirstPage) {
1214                            // First page exists, so question pages start from page 2
1215                            displayPage = Math.max(1, quizData.currentPage - 1);
1216                            displayTotal = quizData.questionPages;
1217                        } else {
1218                            // No first page, so all pages are question pages
1219                            displayPage = quizData.currentPage;
1220                            displayTotal = quizData.questionPages;
1221                        }
1222                        
1223                        let text = 'Page ' + displayPage + ' of ' + displayTotal;
1224                        $counter.text(text);
1225                    }
1226                }
1227            },
1228
1229
1230            /**
1231             * Validate current page
1232             */
1233            validateCurrentPage: function(quizId) {
1234                let quizData = this.quizObjects[quizId];
1235                if (!quizData || !quizData.validation.enabled) return true;
1236
1237                let $currentPage = quizData.pages.eq(quizData.currentPage - 1);
1238                if (!$currentPage.length) {
1239                    return true;
1240                }
1241                
1242                return this.validateElements($currentPage.find('*'), quizId);
1243            },
1244
1245            /**
1246             * Validate entire form (similar to qmnValidation)
1247             */
1248            validateForm: function(quizId) {
1249                let quizData = this.quizObjects[quizId];
1250                if (!quizData) return false;
1251                
1252                // Skip validation if timer limit is enabled and time expired
1253                if (quizData.data.timer_limit_val > 0 && quizData.data.hasOwnProperty('skip_validation_time_expire') && quizData.data.skip_validation_time_expire != 1) {
1254                    return true;
1255                }
1256
1257                return this.validateElements(quizData.form.find('*'), quizId);
1258            },
1259
1260            /**
1261             * Core validation logic (based on qmnValidation)
1262             */
1263            validateElements: function($elements, quizId) {
1264                let quizData = this.quizObjects[quizId];
1265                if (!quizData) return false;
1266                
1267                show_result_validation = true;
1268                jQuery(document).trigger('qsm_before_validation', [$elements, 'quizForm' + quizId]);
1269                let data = quizData.data;
1270                let errorMessages = data.error_messages || {};
1271                
1272                // Clear previous errors
1273                this.resetErrors(quizId);
1274                
1275                let self = this;
1276                $elements.each(function() {
1277                    let $field = $(this);
1278                    let fieldClass = $field.attr('class') || '';
1279                    let fieldValue = $field.val() || '';
1280                    
1281                    // Skip if field is not visible or doesn't have validation classes
1282                    if (!fieldClass || (!$field.is(':visible') && !$field.parent().is(':visible'))) {
1283                        return;
1284                    }
1285                    
1286                    // Email validation
1287                    if (fieldClass.indexOf('mlwEmail') !== -1 && fieldValue !== '') {
1288                        let email = $.trim(fieldValue);
1289                        if (!self.isValidEmail(email)) {
1290                            self.displayError(errorMessages.email_error_text || 'Please enter a valid email address.', $field, quizId);
1291                            show_result_validation = false;
1292                        }
1293                    }
1294
1295                    // Phone number validation
1296                    if (fieldClass.includes('mlwPhoneNumber') && fieldValue !== '') {
1297                        let phoneValue = $.trim(fieldValue);
1298                        let phonePattern = $field.attr('data-phone-pattern');
1299                        if (phonePattern !== undefined && phonePattern !== '') {
1300                            let escapedPattern = phonePattern.replaceAll(/[.+?^$|()\\]/g, String.raw`\$&`);
1301                            escapedPattern = escapedPattern.replaceAll(/ /g, String.raw`\s+`);
1302                            let pattern = new RegExp(String.raw`^${escapedPattern}$`);
1303                                if (!pattern.test(phoneValue)) {
1304                                    self.displayError(errorMessages.phone_error_text || 'Please enter a valid phone number.', $field, quizId);
1305                                    show_result_validation = false;
1306                            }
1307                        }
1308                    }
1309                    
1310                    // URL validation
1311                    if (fieldClass.indexOf('mlwUrl') !== -1 && fieldValue !== '') {
1312                        if (!self.isValidUrl($.trim(fieldValue))) {
1313                            self.displayError(errorMessages.url_error_text || 'Please enter a valid URL.', $field, quizId);
1314                            show_result_validation = false;
1315                        }
1316                    }
1317                    
1318                    // Min length validation
1319                    if (fieldClass.indexOf('mlwMinLength') !== -1 && fieldValue !== '') {
1320                        let minLength = Number.parseInt($field.attr('minlength')) || 0;
1321                        if ($.trim(fieldValue).length < minLength) {
1322                            let message = (errorMessages.minlength_error_text || 'Minimum %minlength% characters required.').replace('%minlength%', minLength);
1323                            self.displayError(message, $field, quizId);
1324                            show_result_validation = false;
1325                        }
1326                    }
1327                    
1328                    // Max length validation
1329                    if (fieldClass.indexOf('mlwMaxLength') !== -1 && fieldValue !== '') {
1330                        let maxLength = Number.parseInt($field.attr('maxlength')) || 0;
1331                        if ($.trim(fieldValue).length > maxLength) {
1332                            let message = (errorMessages.maxlength_error_text || 'Maximum %maxlength% characters allowed.').replace('%maxlength%', maxLength);
1333                            self.displayError(message, $field, quizId);
1334                            show_result_validation = false;
1335                        }
1336                    }
1337                    
1338                    // Required field validations
1339                    let isContactField = $field.closest('.qsm_contact_div').length > 0;
1340                    let requiredErrorMsg = (isContactField && errorMessages.contact_field_required_error_text) 
1341                        ? errorMessages.contact_field_required_error_text 
1342                        : (errorMessages.empty_error_text || 'Please complete all required fields!');
1343
1344                    if (fieldClass.indexOf('mlwRequiredText') !== -1 && $.trim(fieldValue) === '') {
1345                        self.displayError(requiredErrorMsg, $field, quizId);
1346                        show_result_validation = false;
1347                    }
1348
1349                    if (fieldClass.indexOf('mlwRequiredPolar') !== -1 && $.trim(fieldValue) === '') {
1350                        self.displayError(errorMessages.empty_error_text || 'This field is required.', $field, quizId);
1351                        show_result_validation = false;
1352                    }
1353                    
1354                    if (fieldClass.indexOf('mlwRequiredNumber') !== -1 && (fieldValue === '' || Number.isNaN(fieldValue))) {
1355                        self.displayError(errorMessages.number_error_text || 'Please enter a valid number.', $field, quizId);
1356                        show_result_validation = false;
1357                    }
1358                    
1359                    if (fieldClass.indexOf('mlwRequiredDate') !== -1 && fieldValue === '') {
1360                        self.displayError(errorMessages.empty_error_text || 'This field is required.', $field, quizId);
1361                        show_result_validation = false;
1362                    }
1363                    
1364                    if (fieldClass.indexOf('mlwRequiredAccept') !== -1 && !$field.prop('checked')) {
1365                        self.displayError(errorMessages.empty_error_text || 'You must accept this.', $field, quizId);
1366                        show_result_validation = false;
1367                    }
1368                    
1369                    if (fieldClass.indexOf('mlwRequiredRadio') !== -1) {
1370                        let checkedVal = $field.find('input:checked').val();
1371                        if (!checkedVal) {
1372                            self.displayError(errorMessages.empty_error_text || 'Please select an option.', $field, quizId);
1373                            show_result_validation = false;
1374                        }
1375                    }
1376
1377                    if (fieldClass.indexOf('qsmRequiredSelect') !== -1) {
1378                        let checkedVal = $field.find('select').val() || $field.filter('select').val();
1379                        if (!checkedVal) {
1380                            self.displayError(errorMessages.empty_error_text || 'Please select an option.', $field, quizId);
1381                            show_result_validation = false;
1382                        }
1383                    }
1384
1385                    if (fieldClass.indexOf('mlwRequiredCheck') !== -1) {
1386                        let checkedVal = $field.find('input:checked').val();
1387                        if (!checkedVal) {
1388                            self.displayError(errorMessages.empty_error_text || 'Please select an option.', $field, quizId);
1389                            show_result_validation = false;
1390                        }
1391                    }
1392                    
1393                    if (fieldClass.indexOf('mlwRequiredFileUpload') !== -1) {
1394                        let files = $field.get(0).files;
1395                        if (!files || files.length === 0) {
1396                            self.displayError(errorMessages.empty_error_text || 'Please select a file.', $field, quizId);
1397                            show_result_validation = false;
1398                        }
1399                    }
1400                });
1401                jQuery(document).trigger('qsm_after_validation', [$elements, 'quizForm' + quizId]);
1402                return show_result_validation;
1403            },
1404
1405
1406            /**
1407             * Display validation error (similar to qmnDisplayError)
1408             */
1409            displayError: function(message, $field, quizId) {
1410                let quizData = this.quizObjects[quizId];
1411                if (!quizData) return;
1412                
1413                // Add error class to field
1414                $field.parents('.quiz_section').addClass('qmn_error');
1415                
1416                // Create error message element
1417                quizData.form.find('.qmn_error_message_section').addClass('qmn_error_message');
1418                quizData.form.find('.qmn_error_message').text(message);
1419            },
1420
1421            /**
1422             * Reset all errors (similar to qmnResetError)
1423             */
1424            resetErrors: function(quizId) {
1425                let quizData = this.quizObjects[quizId];
1426                if (!quizData) return;
1427                
1428                quizData.firstErrorFocused = false;
1429                
1430                // Remove error messages
1431                quizData.form.find('.qmn_error_message').text('');
1432                quizData.form.find('.qmn_error_message_section').removeClass('qmn_error_message');
1433                
1434                // Remove error classes
1435                quizData.form.find('.qmn_error').removeClass('qmn_error');
1436            },
1437
1438            clearPageErrors: function(quizId) {
1439                let quizData = this.quizObjects[quizId];
1440                if (!quizData) return;
1441
1442                let $currentPage = quizData.pages.eq(quizData.currentPage - 1);
1443                $currentPage.find('.qmn_error').removeClass('qmn_error');
1444                quizData.form.find('.qmn_error_message').text('');
1445                quizData.form.find('.qmn_error_message_section').removeClass('qmn_error_message');
1446            },
1447
1448            clearFieldError: function($field) {
1449                $field.removeClass('qmn_error');
1450            },
1451
1452            /**
1453             * Validation helper methods
1454             */
1455            isValidEmail: function(email) {
1456                let regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1457                return regex.test(email);
1458            },
1459
1460            isValidUrl: function(url) {
1461                const regex = /^(https?|ftp):\/\/[^\s/?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?$/i;
1462                return regex.test(url);
1463            },
1464
1465            /**
1466             * Handle keyboard navigation
1467             */
1468            handleKeyboardNavigation: function(e, quizId) {
1469                let quizData = this.quizObjects[quizId];
1470                if (!quizData) {
1471                    return;
1472                }
1473
1474                let qsm_keypress_navigation_action = { allowed: true };
1475                $(document).trigger('qsm_keyboard_navigation_action_start', [e, qsm_keypress_navigation_action]);
1476                if (qsm_keypress_navigation_action.allowed === false) {
1477                    return;
1478                }
1479
1480                // Don't interfere with keyboard navigation inside contact fields
1481                if ($(e.target).is('input, textarea, select') && $(e.target).closest('div.qsm_contact_div').length > 0) {
1482                    return;
1483                }
1484
1485                let isTypingField = $(e.target).is('textarea, select, input[type="text"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], input[type="password"], [contenteditable="true"]');
1486
1487                // Allow Ctrl+Enter to submit form
1488                if (e.ctrlKey && e.keyCode === 13) {
1489                    e.preventDefault();
1490                    quizData.form.trigger('submit');
1491                    return;
1492                }
1493                const tag = e.target.tagName.toLowerCase();
1494                const isEditable =
1495                    tag === 'textarea' ||
1496                    (tag === 'input' && (
1497                        e.target.type === 'text' || 
1498                        e.target.type === 'email' || 
1499                        e.target.type === 'number' || 
1500                        e.target.type === 'search' || 
1501                        e.target.type === 'tel' || 
1502                        e.target.type === 'url' || 
1503                        e.target.type === 'password' ||
1504                        e.target.type === 'file'
1505                    )) && !['button', 'submit', 'checkbox', 'radio'].includes(e.target.type);
1506
1507                if (e.keyCode === 13 && !isEditable) {
1508                e.preventDefault();
1509
1510                let $container = quizData.quizContainer;
1511                let $startBtn = $container.find('.qsm-start-btn:visible, .qsm-start-btn-' + quizId + ':visible');
1512                let $submitBtn = $container.find('.qsm-submit-btn:visible, .qsm-submit-btn-' + quizId + ':visible');
1513                let $nextBtn = $container.find('.qsm-next-btn:visible, .qsm-next-btn-' + quizId + ':visible');
1514
1515                // Priority order: Start button → Next button → Submit button (only on last page)
1516                if ($startBtn.length) {
1517                    $startBtn.first().trigger('click');
1518                    return;
1519                }
1520                if ($nextBtn.length) {
1521                    $nextBtn.first().trigger('click');
1522                    return;
1523                }
1524                // Only submit if on last page (no next button)
1525                if ($submitBtn.length && !$nextBtn.length) {
1526                    $submitBtn.first().trigger('click');
1527                    return;
1528                }
1529
1530                // Fallback
1531                this.nextPage(quizId);
1532                return;
1533            }
1534
1535            // Left/Right arrow keys to navigate pages (avoid interfer
1535ing with typing/caret movement)
1536            // Arrow keys should NEVER submit, only navigate
1537            if ((e.keyCode === 37 || e.keyCode === 39) && !isTypingField) {
1538                e.preventDefault();
1539                let $container = quizData.quizContainer;
1540                if (e.keyCode === 39) {
1541                    // Arrow Right - check for start button or next page
1542                    let $startBtn = $container.find('.qsm-start-btn:visible, .qsm-start-btn-' + quizId + ':visible');
1543                    let $nextBtn = $container.find('.qsm-next-btn:visible, .qsm-next-btn-' + quizId + ':visible');
1544                    
1545                    if ($startBtn.length) {
1546                        $startBtn.first().trigger('click');
1547                        return;
1548                    }
1549                    // Only navigate to next page if next button exists, never submit
1550                    if ($nextBtn.length) {
1551                        this.nextPage(quizId);
1552                    }
1553                } else {
1554                    // Arrow Left - navigate to previous page
1555                    this.previousPage(quizId);
1556                }
1557                return;
1558            }
1559
1560            // Tab / Shift+Tab cycles through individual elements: question inputs → contact fields → navigation buttons
1561            if (e.keyCode === 9) {
1562                let $currentPage = quizData.pages.eq(quizData.currentPage - 1);
1563                let $container = quizData.quizContainer;
1564
1565                // Get all individual focusable elements from questions (inputs, selects, textareas, buttons, radios, checkboxes)
1566                let $questionInputs = $currentPage.find('.qsm-question-wrapper:visible').find('input, select, textarea, button, a[href]').filter(':visible');
1567
1568                // Contact form fields
1569                let $contactFields = $currentPage.find('.qsm_contact_div').find('input, select, textarea, button, a[href]').filter(':visible');
1570
1571                // Navigation buttons
1572                let $prevBtn = $container.find('.qsm-previous-btn:visible, .qsm-previous:visible, .qsm-previous-btn-' + quizId + ':visible').first();
1573                let $nextBtn = $container.find('.qsm-next-btn:visible, .qsm-next:visible, .qsm-next-btn-' + quizId + ':visible').first();
1574                let $submitBtn = $container.find('.qsm-submit-btn:visible, .qsm-submit-btn-' + quizId + ':visible').first();
1575                let $startBtn = $container.find('.qsm-start-btn:visible, .qsm-start-btn-' + quizId + ':visible').first();
1576
1577                // Build ordered list of all individual focusable elements
1578                let focusOrder = [];
1579
1580                // Add question inputs (each individual element)
1581                $questionInputs.each(function() { focusOrder.push($(this)); });
1582
1583                // Add contact fields
1584                $contactFields.each(function() { focusOrder.push($(this)); });
1585
1586                // Add navigation buttons
1587                if ($prevBtn.length) focusOrder.push($prevBtn);
1588                if ($startBtn.length) {
1589                    focusOrder.push($startBtn);
1590                } else if ($submitBtn.length) {
1591                    focusOrder.push($submitBtn);
1592                } else if ($nextBtn.length) {
1593                    focusOrder.push($nextBtn);
1594                }
1595
1596                if (focusOrder.length === 0) return;
1597
1598                // Find current focus position
1599                let currentIndex = -1;
1600                let $activeElement = $(document.activeElement);
1601                for (let i = 0; i < focusOrder.length; i++) {
1602                    if (focusOrder[i].is($activeElement)) {
1603                        currentIndex = i;
1604                        break;
1605                    }
1606                }
1607
1608                // If no focusable element is focused, start from beginning
1609                if (currentIndex === -1) {
1610                    e.preventDefault();
1611                    focusOrder[0].focus();
1612                    // Update active question highlighting
1613                    let $wrapper = focusOrder[0].closest('.qsm-question-wrapper');
1614                    if ($wrapper.length) {
1615                        $currentPage.find('.qsm-question-wrapper').removeClass('qsm-active-question qsm-last-active-question');
1616                        $wrapper.addClass('qsm-active-question');
1617                    }
1618                    return;
1619                }
1620
1621                e.preventDefault();
1622
1623                // Calculate next index
1624                let nextIndex;
1625                if (e.shiftKey) {
1626                    nextIndex = currentIndex - 1;
1627                    if (nextIndex < 0) nextIndex = focusOrder.length - 1;
1628                } else {
1629                    nextIndex = currentIndex + 1;
1630                    if (nextIndex >= focusOrder.length) nextIndex = 0;
1631                }
1632
1633                // Focus the next element
1634                focusOrder[nextIndex].focus();
1635
1636                // Update active question highlighting for the newly focused element
1637                let $nextWrapper = focusOrder[nextIndex].closest('.qsm-question-wrapper');
1638                if ($nextWrapper.length) {
1639                    $currentPage.find('.qsm-question-wrapper').removeClass('qsm-active-question qsm-last-active-question');
1640                    $nextWrapper.addClass('qsm-active-question');
1641                }
1642
1643                return;
1644            }
1645        },
1646
1647            /**
1648             * Scroll to quiz form
1649             */
1650            scrollToQuiz: function(quizId) {
1651                let quizData = this.quizObjects[quizId];
1652                if (!quizData) return;
1653
1654                $('html, body').animate({
1655                    scrollTop: quizData.form.offset().top - 50
1656                }, this.config.animation.duration);
1657            },
1658
1659            /**
1660             * Manage focus for accessibility
1661             */
1662            manageFocus: function(quizId) {
1663                let quizData = this.quizObjects[quizId];
1664                if (!quizData) return;
1665
1666                let $currentPage = quizData.pages.eq(quizData.currentPage - 1);
1667                
1668                $currentPage.css('outline', 'none').attr('tabindex', '-1').focus();
1669            },
1670
1671            /**
1672             * Submit the form (similar to qmnFormSubmit)
1673             */
1674            submitForm: function(quizId) {
1675                let quizData = this.quizObjects[quizId];
1676                if (!quizData) return;
1677                
1678                let self = this;
1679                if (!self.validateForm(quizId)) {
1680                    return false;
1681                }
1682                // Trigger before submit event
1683                $(document).trigger('qsm_before_quiz_submit', ['quizForm' + quizId]);
1684                
1685                // Disable submit button to prevent double submission
1686                quizData.form.find('input[type="submit"], .qsm-submit').prop('disabled', true);
1687                quizData.quizContainer.find('input[type="submit"], .qsm-submit').prop('disabled', true);
1688                
1689                // Enable all form fields for submission (except submit buttons)
1690                quizData.form.find('input:not([type="submit"]), select, textarea').prop('disabled', false);
1691                
1692                // Prepare form data - ensure all form fields are included
1693                let formData = new FormData();
1694                
1695                // Add all form inputs, selects, and textareas
1696                quizData.form.find('input, select, textarea').each(function() {
1697                    let $field = $(this);
1698                    let name = $field.attr('name');
1699                    let type = $field.attr('type');
1700                    
1701                    if (!name) return; // Skip fields without names
1702                    
1703                    if (type === 'radio' || type === 'checkbox') {
1704                        if ($field.is(':checked')) {
1705                            formData.append(name, $field.val());
1706                        }
1707                    } else if (type === 'file') {
1708                        let files = this.files;
1709                        if (files && files.length > 0) {
1710                            formData.append(name, files[0]);
1711                        }
1712                    } else if (type !== 'submit' && type !== 'button') {
1713                        formData.append(name, $field.val() || '');
1714                    }
1715                });
1716                
1717                // Add quiz ID
1718                formData.append('quiz_id', quizId);
1719                
1720                // Add quiz start date
1721                formData.append('quiz_start_date', localStorage.getItem('mlw_quiz_start_date' + quizId));
1722                
1723                // Add timer data if available
1724                let timerInfo = QSMPagination && QSMPagination.Timer ? 
1725                            QSMPagination.Timer.getTimerInfo(quizId) : null;
1726                if (timerInfo) {
1727                    formData.append('timer_ms', (timerInfo.totalTime - timerInfo.remainingTime) * 1000);
1728                }
1729                
1730                // Add required fields for QSM processing (matching legacy exactly)
1731                formData.append('action', 'qmn_process_quiz');
1732                formData.append('nonce', quizData.form.find('#qsm_nonce_' + quizId).val() || quizData.nonceValue || '');
1733                formData.append('qsm_unique_key', quizData.form.find('#qsm_unique_key_' + quizId).val() || '');
1734                formData.append('currentuserTime', Math.round(new Date().getTime() / 1000));
1735                formData.append('currentuserTimeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
1736                
1737                // Add timer field if it exists
1738                let timerField = quizData.form.find('#timer').length ? 
1739                            quizData.form.find('#timer') : 
1740                            quizData.quizContainer.find('#timer');
1741                if (timerField.length) {
1742                    formData.append('timer', timerField.val() || '0');
1743                }
1744                
1745                // Trigger after form data process event (like legacy)
1746                $(document).trigger('qsm_after_form_data_process', ['qsmForm' + quizId, formData]);
1747                
1748                // End timer (matching legacy behavior)
1749                if (QSMPagination && QSMPagination.Timer) {
1750                    QSMPagination.Timer.endTimer(quizId);
1751                }
1752                
1753                // Show loading state
1754                this.showLoading(quizId);
1755                
1756                // Hide all micromodal popups
1757                $('.qsm-popup').removeClass('is-open');
1758
1759                // Get AJAX URL from quiz data or global
1760                let ajaxUrl = quizData.data.ajax_url || 
1761                            (typeof qmn_ajax_object !== 'undefined' ? qmn_ajax_object.ajaxurl : '') ||
1762                            '/wp-admin/admin-ajax.php';
1763                
1764                // Submit via AJAX
1765                $.ajax({
1766                    url: ajaxUrl,
1767                    data: formData,
1768                    contentType: false,
1769                    processData: false,
1770                    type: 'POST',
1771                    success: function(response) {
1772                        try {
1773                            if (typeof response === 'string') {
1774                                response = JSON.parse(response);
1775                            }
1776                            if (window.qsm_results_data === undefined) {
1777                                window.qsm_results_data = new Object();
1778                            }
1779                            window.qsm_results_data[quizId] = {
1780                                'save_response': response.result_status['save_response'],
1781                                'id': response.result_status['id']
1782                            };
1783                            if (response.quizExpired) {
1784                                if (typeof MicroModal !== 'undefined') {
1785                                    MicroModal.show('modal-4');
1786                                }
1787                                return;
1788                            }
1789                            
1790                            // Check if response has expected structure
1791                            if (!response || typeof response !== 'object') {
1792                                throw new Error('Invalid response format received from server');
1793                            }
1794
1795                            // Display results
1796                            self.displayResults(response, quizId);
1797                            
1798                            // Trigger after submit event
1799                            $(document).trigger('qsm_after_quiz_submit', ['quizForm' + quizId]);
1800                        
1801                        } catch (e) {
1802                            console.log(e);
1803                            self.displayError('Error processing quiz results: ' + e.message + '. Please check console for details.', quizId);
1804                        }
1805                    },
1806                    error: function(xhr, status, error) {
1807                        // Re-enable submit button on error
1808                        quizData.quizContainer.find('input[type="submit"], .qsm-submit').prop('disabled', false);
1809                        
1810                        // Show error message
1811                        let errorMessage = 'Error submitting quiz: ' + error + ' (Status: ' + xhr.status + ')';
1812                        if (xhr.responseText) {
1813                            errorMessage += ' - Response: ' + xhr.responseText.substring(0, 200);
1814                        }
1815                        self.displayError(errorMessage, quizId);
1816                    }
1817                });
1818            },
1819
1820            /**
1821             * Display results
1822             */
1823            displayResults: function(response, quizId) {
1824                let quizData = this.quizObjects[quizId];
1825                if (!quizData) return;
1826                
1827                let $quizContainer = quizData.quizContainer;
1828                jQuery(document).trigger('qsm_before_display_result', [response, 'quizForm' + quizId, $quizContainer]);
1829                if (response.redirect && response.redirect != '') {
1830                    window.location.replace(response.redirect);
1831                } else {
1832                    // Clear existing content
1833                    $quizContainer.find('.qsm_results_page').remove();
1834                    
1835                    // Create results container
1836                    let $resultDiv = $('<div class="qsm_results_page qmn_results_page">');
1837                
1838                    $quizContainer.find('.qsm-quiz-processing-box').remove();
1839                    $resultDiv.html(response.display);
1840                    $quizContainer.append($resultDiv);
1841                    $resultDiv.slideDown();
1842                
1843                    // Clean up localStorage
1844                    if( localStorage.getItem('mlw_quiz_start_date' + quizId) ) {
1845                        localStorage.removeItem('mlw_quiz_start_date' + quizId);
1846                    }
1847                
1848                    // Scroll to results
1849                    $('html, body').animate({
1850                        scrollTop: $resultDiv.offset().top - 50
1851                    }, 500);
1852                
1853                    // Trigger after result is displayed - matches legacy qsm-quiz.js format
1854                    jQuery(document).trigger('qsm_after_display_result', [response, 'quizForm' + quizId, $quizContainer]);
1855                }
1856            },
1857
1858            /**
1859             * Show loading state
1860             */
1861            showLoading: function(quizId) {
1862                let quizData = this.quizObjects[quizId];
1863                if (!quizData) return;
1864                
1865                let loadingHtml = '<div class="qsm-quiz-processing-box">';
1866                loadingHtml += '<div class="qsm-spinner-loader"></div>';
1867                loadingHtml += '<div class="qsm-processing-message">Processing your quiz...</div>';
1868                loadingHtml += '</div>';
1869                
1870                quizData.quizContainer.html(loadingHtml);
1871            },
1872
1873            /**
1874             * Handle window resize
1875             */
1876            handleResize: function() {
1877                Object.keys(this.quizObjects).forEach(function(quizId) {
1878                    $(document).trigger('qsm_navigation_resize', [quizId, this.quizObjects[quizId]]);
1879                }.bind(this));
1880            },
1881
1882
1883            /**
1884             * Get quiz ID from container or form element
1885             */
1886            getQuizId: function($element) {
1887                // Try data attribute first (for containers)
1888                let id = $element.data('quiz-id');
1889                if (id) {
1890                    return id;
1891                }
1892                
1893                // Try form ID (if element is a form)
1894                id = $element.attr('id');
1895                if (id && (id.indexOf('qsmForm') === 0 || id.indexOf('qsmNewForm') === 0)) {
1896                    return id.replace(/qsm(New)?Form/, '');
1897                }
1898                
1899                // Try finding form within container
1900                let $form = $element.find('.qsm-quiz-form');
1901                if ($form.length) {
1902                    id = $form.attr('id');
1903                    if (id && (id.indexOf('qsmForm') === 0 || id.indexOf('qsmNewForm') === 0)) {
1904                        return id.replace(/qsm(New)?Form/, '');
1905                    }
1906                }
1907                
1908                // Try parent container data attribute
1909                let $container = $element.closest('[data-quiz-id]');
1910                if ($container.length) {
1911                    return $container.data('quiz-id');
1912                }
1913                
1914                return null;
1915            },
1916
1917            /**
1918             * Show inline result feedback (matching legacy qsm_show_inline_result)
1919             */
1920            qsmShowInlineResult: function(quizId, question_id, value, $this, answer_type, $i_this, index = null) {
1921                if ($i_this?.hasClass?.('mlw_answer_date')) {
1922                    if (!/^[1-9]\d{3}-\d{2}-\d{2}$/.test(value)) {
1923                        return;
1924                    }
1925                }
1926                $('.qsm-spinner-loader').remove();
1927                this.addSpinnerLoader($this, $i_this);
1928                
1929                let quizData = this.getQuizData(quizId);
1930                let data = this.qsmQuestionQuickResultJs(question_id, value, answer_type, quizData.enable_quick_correct_answer_info, quizId, index);
1931                
1932                $this.find('.quick-question-res-p, .qsm-inline-correct-info').remove();
1933                $this.find('.qmn_radio_answers, .qsm-answer-options').children().removeClass('data-correct-answer');
1934                
1935                if (value && value.length > 0 && data.success == 'correct') {
1936                    $this.append('<div style="color: green" class="quick-question-res-p qsm-correct-answer-info">' + quizData.quick_result_correct_answer_text + '</div>');
1937                    $this.append('<div class="qsm-inline-correct-info">' + this.qsmCheckShortcode(data.message) + '</div>');
1938                } else if (value && value.length > 0 && data.success == 'incorrect') {
1939                    $this.find('.qmn_radio_answers input[value="' + data.correct_index + '"], .qsm-answer-options input[value="' + data.correct_index + '"]').parent().addClass('data-correct-answer');
1940                    $this.append('<div style="color: red" class="quick-question-res-p qsm-incorrect-answer-info">' + quizData.quick_result_wrong_answer_text + '</div>');
1941                    $this.append('<div class="qsm-inline-correct-info">' + this.qsmCheckShortcode(data.message) + '</div>');
1942                }
1943                
1944                // Render MathJax if enabled
1945                if (Number(quizData.disable_mathjax) !== 1 && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
1946                    MathJax.typesetPromise();
1947                }
1948                
1949                $('.qsm-spinner-loader').remove();
1950                $(document).trigger('qsm_show_inline_result_after', [quizId, question_id, value, $this, answer_type, $i_this, index]);
1951            },
1952            
1953            /**
1954             * Add spinner loader (matching legacy addSpinnerLoader)
1955             */
1956            addSpinnerLoader: function($this, $i_this) {
1957                if ($this.find('.mlw_answer_open_text').length) {
1958                    $this.find('.mlw_answer_open_text').after('<div class="qsm-spinner-loader" style="font-size: 2.5px;margin-left:10px;"></div>');
1959                } else if ($this.find('.mlw_answer_number').length) {
1960                    $this.find('.mlw_answer_number').after('<div class="qsm-spinner-loader" style="font-size: 2.5px;margin-left:10px;"></div>');
1961                } else {
1962                    $i_this.next('.qsm-input-label').after('<div class="qsm-spinner-loader" style="font-size: 2.5px;"></div>');
1963                }
1964            },
1965            
1966            /**
1967             * Check answer correctness using encrypted data (matching legacy qsm_question_quick_result_js)
1968             */
1969            /**
1970             * Format date answer to YYYY-MM-DD.
1971             */
1972            qsmFormatDateAnswer: function(v) {
1973                if (typeof v === 'string') {
1974                    let ymd = /^([1-9]\d{3})[-/](\d{2})[-/](\d{2})$/.exec(v);
1975                    if (ymd) {
1976                        return ymd[1] + '-' + ymd[2] + '-' + ymd[3];
1977                    }
1978                    let dmy = /^(\d{2})[-/](\d{2})[-/]([1-9]\d{3})$/.exec(v);
1979                    if (dmy) {
1980                        return dmy[3] + '-' + dmy[2] + '-' + dmy[1];
1981                    }
1982                }
1983                return v;
1984            },
1985
1986            qsmQuestionQuickResultJs: function(question_id, answer, answer_type, show_correct_info, quiz_id, ans_index = null) {
1987                answer_type = answer_type || '';
1988                show_correct_info = show_correct_info || '';
1989                
1990                if (typeof encryptedData === 'undefined' || typeof encryptedData[quiz_id] === 'undefined') {
1991                    return { correct_index: 0, success: '', message: '' };
1992                }
1993                
1994                try {
1995                    let decryptedBytes = CryptoJS.AES.decrypt(encryptedData[quiz_id], encryptionKey[quiz_id]);
1996                    let decryptedData = decryptedBytes.toString(CryptoJS.enc.Utf8);
1997                    let decrypt = JSON.parse(decryptedData);
1998                    
1999                    question_id = typeof question_id !== 'undefined' ? Number.parseInt(question_id) : 0;
2000                    answer = typeof answer !== 'undefined' ? answer : '';
2001                    
2002                    let answer_array = decrypt[question_id].answer_array;
2003                    let settings = decrypt[question_id].settings;
2004                    let correct_info_text = decrypt[question_id].correct_info_text;
2005                    let correct_answer_logic = decrypt.correct_answer_logic;
2006                    show_correct_info = typeof show_correct_info !== 'undefined' && show_correct_info != 0 ? show_correct_info : '';
2007                    
2008                    let got_ans = false;
2009                    let correct_answer = false;
2010                    let count = 0;
2011                    let correct_index = 0;
2012                    let answer_count = 0;
2013                    let total_correct_answer = 0;
2014                    
2015                    if (answer_array && false === got_ans) {
2016                        for (let key in answer_array) {
2017                            let value = answer_array[key];
2018                            
2019                            if ('input' === answer_type) {
2020                                if (!settings['case_sensitive']) {
2021                                    answer = answer.toUpperCase();
2022                                    value[0] = value[0].toUpperCase();
2023                                }
2024                                
2025                                let isDateQuestion = 12 === Number.parseInt(decrypt[question_id].question_type_new);
2026                                let correctValue = isDateQuestion ? this.qsmFormatDateAnswer(value[0]) : value[0];
2027
2028                                if (answer == correctValue && (1 === Number.parseInt(value[2]) || 14 === Number.parseInt(decrypt[question_id].question_type_new)) && (!settings['matchAnswer'] || 'random' === settings['matchAnswer'] || key == ans_index)) {
2029                                    got_ans = true;
2030                                    correct_answer = true;
2031                                    break;
2032                                }
2033                            } else if ('checkbox' === answer_type) {
2034                                if (0 == correct_answer_logic) {
2035                                    for (let anskey in answer) {
2036                                        let ansvalue = answer[anskey];
2037                                        if (Number.parseInt(ansvalue) === Number.parseInt(key) && 1 == value[2]) {
2038                                            got_ans = true;
2039                                            correct_answer = true;
2040                                            break;
2041                                        }
2042                                    }
2043                                } else {
2044                                    if (answer_array[answer[key]] !== undefined) {
2045                                        if (1 == answer_array[answer[key]][2]) {
2046                                            answer_count++;
2047                                        } else if (answer[key] !== undefined) {
2048                                            answer_count--;
2049                                        }
2050                                    }
2051                                    if (1 == value[2]) {
2052                                        total_correct_answer++;
2053                                    }
2054                                }
2055                            } else if (Number.parseInt(answer) === Number.parseInt(key) && 1 === Number.parseInt(value[2])) {
2056                                got_ans = true;
2057                                correct_answer = true;
2058                                break;
2059                            }
2060                        }
2061                        
2062                        for (let key in answer_array) {
2063                            let value = answer_array[key];
2064                            if (false == correct_answer) {
2065                                if (1 == value[2]) {
2066                                    correct_index = count;
2067                                }
2068                                count++;
2069                            }
2070                        }
2071                        
2072                        if ('checkbox' === answer_type) {
2073                            if (1 == correct_answer_logic) {
2074                                if (0 != answer_count && 0 != total_correct_answer && total_correct_answer == answer_count) {
2075                                    got_ans = true;
2076                                    correct_answer = true;
2077                                }
2078                            }
2079                        }
2080                    }
2081                    
2082                    if (2 == show_correct_info) {
2083                        got_ans = true;
2084                    }
2085                    
2086                    let returnObject = {
2087                        "correct_index": correct_index,
2088                        "success": correct_answer ? 'correct' : 'incorrect',
2089                        "message": show_correct_info && got_ans ? correct_info_text : ""
2090                    };
2091                    
2092                    $(document).trigger('qsm_question_quick_result_js_after', [returnObject, correct_answer, answer, answer_array, answer_type, settings, decrypt, question_id]);
2093                    return returnObject;
2094                } catch (e) {
2095                    return { correct_index: 0, success: '', message: '' };
2096                }
2097            },
2098            
2099            /**
2100             * Submit quiz if answer is wrong (matching legacy qsm_submit_quiz_if_answer_wrong)
2101             */
2102            qsmSubmitQuizIfAnswerWrong: function(quizId, question_id, value, $this, $quizForm, answer_type = '') {
2103                answer_type = answer_type || '';
2104                let quizData = this.getQuizData(quizId);
2105                
2106                let data = this.qsmQuestionQuickResultJs(question_id, value, answer_type, quizData.enable_quick_correct_answer_info, quizId);
2107                
2108                this.changes(data, question_id.replace(/\D/g, ""), quizId);
2109                
2110                if (data.success == 'incorrect') {
2111                    $quizForm.closest('.qsm-quiz-container').find('[class*="Required"]').removeClass();
2112                    $quizForm.closest('.qsm-quiz-container').find('.qsm-submit-btn').trigger('click');
2113                }
2114                
2115                // Render MathJax if enabled
2116                if (Number(quizData.disable_mathjax) !== 1 && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
2117                    MathJax.typesetPromise();
2118                }
2119            },
2120            
2121            /**
2122             * Initialize q_counter model (matching legacy q_counter)
2123             */
2124            q_counter: Backbone.Model.extend({
2125                defaults: {
2126                    answers: []
2127                }
2128            }),
2129
2130            /**
2131             * Changes function (matching legacy changes)
2132             */
2133            changes: function(data, question_id, quiz_id) {
2134                let quizData = this.getQuizData(quiz_id);
2135                let answers = qsmLogicModel.get('answers');
2136                
2137                answers.push({
2138                    'q_id': question_id,
2139                    'incorrect': data.success == 'correct' ? 0 : 1,
2140                });
2141                
2142                qsmLogicModel.set({ 'answers': this.filter_question(qsmLogicModel.get('answers')) });
2143                
2144                let update_answers = qsmLogicModel.get('answers');
2145                let incorrect = 0;
2146
2147                update_answers.forEach(function(obj){
2148                    if(obj.incorrect == 1){
2149                        incorrect++;
2150                    }
2151                });
2152
2153                if( Number(quizData.end_quiz_if_wrong) <= incorrect ) {
2154                    this.submit_status = true;
2155                }else{
2156                    this.submit_status = false;
2157                }
2158            },
2159
2160            filter_question: function(arr) {
2161                let result = {};
2162                arr.forEach(function(obj) {
2163                    if (obj.q_id) {
2164                        result[obj.q_id] = obj;
2165                    }
2166                });
2167                return Object.values(result);
2168            },
2169
2170            /**
2171             * Process shortcodes in message (matching legacy qsm_check_shortcode)
2172             */
2173            qsmCheckShortcode: function(message) {
2174                if (!message) return '';
2175                
2176                const videoContentRegex = /\[video(?:\s(?:src|mp4|m4v|webm|ogv|wmv|flv|width|height)="[^"]*")*\](.*?)\[\/video\]/g;
2177                let videoMatch = message.match(videoContentRegex);
2178
2179                if (videoMatch) {
2180                    let videoHTML = message.replace(videoContentRegex, function(match, content) {
2181                        const { src, width, height } = this.parseAttributes(match);
2182                        const videoTag = this.generateVideoTag(src, width, height, content);
2183                        return `<div class="video-content">${videoTag}</div>`;
2184                    });
2185                    return videoHTML;
2186                }
2187
2188                // Check if message contains an image shortcode
2189                let imageRegex = /\[img(?:(?:\ssrc="([^"]+)")|(?:\salt="([^"]+)")|(?:\swidth="(\d+)")|(?:\sheight="(\d+)")){0,4}\s*\]/g;
2190                let imageMatch = message.match(imageRegex);
2191
2192                if (imageMatch) {
2193                    let imageHTML = message.replace(imageRegex, function(match, src, alt, width, height) {
2194                        return '<img src="' + (src || '') + '" alt="' + (alt || '') + '" width="' + (width || '') + '" height="' + (height || '') + '">';
2195                    });
2196                    return '<div class="image-content">' + imageHTML + '</div>';
2197                }
2198
2199                return message;
2200            },
2201
2202            /**
2203             * Parse attributes from video shortcode
2204             */
2205            parseAttributes: function(match) {
2206                let videoAttrs = { src: '', width: '', height: '' };
2207
2208                videoAttributePatterns.forEach(pattern => {
2209                    const attrMatch = match.match(pattern);
2210                    if (attrMatch) {
2211                        const value = attrMatch[1] || '';
2212                        if (pattern.toString().includes('width')) {
2213                            videoAttrs.width = value;
2214                        } else if (pattern.toString().includes('height')) {
2215                            videoAttrs.height = value;
2216                        } else {
2217                            videoAttrs.src = value;
2218                        }
2219                    }
2220                });
2221
2222                return videoAttrs;
2223            },
2224
2225            /**
2226             * Generate video tag from shortcode
2227             */
2228            generateVideoTag: function(src, width, height, content) {
2229                return `<video src="${src}" width="${width}" height="${height}" controls>${content}</video>`;
2230            },
2231
2232            /**
2233             * Get validation rules for quiz
2234             */
2235            getValidationRules: function(quizId) {
2236                // This can be extended based on quiz configuration
2237                return {
2238                    required: true,
2239                    email: true,
2240                    url: true,
2241                    number: true
2242                };
2243            },
2244
2245            /**
2246             * Destroy quiz instance
2247             */
2248            destroy: function(quizId) {
2249                
2250                if (this.quizObjects[quizId]) {
2251                    // Remove event listeners
2252                    this.quizObjects[quizId].form.off();                
2253
2254                    // Delete instance
2255                    delete this.quizObjects[quizId];                
2256                    
2257                    // Trigger destroy event
2258                    $(document).trigger('qsm_navigation_destroyed', [quizId]);
2259                }
2260            }
2261        }
2262    };
2263
2264    var qsmLogicModel = new QSMPagination.Navigation.q_counter({});
2265
2266    /**
2267     * jQuery debounce function
2268     */
2269    $.debounce = function(delay, fn) {
2270        let timer = null;
2271        return function() {
2272            let context = this, args = arguments;
2273            clearTimeout(timer);
2274            timer = setTimeout(function() {
2275                fn.apply(context, args);
2276            }, delay);
2277        };
2278    };
2279
2280    /**
2281     * Initialize on document ready
2282     */
2283    $(document).ready(function() {
2284        QSMPagination.Navigation.init();
2285    });
2286
2287    /**
2288     * Re-initialize on AJAX content load
2289     */
2290    $(document).on('qsm_content_loaded', function() {
2291        QSMPagination.Navigation.init();
2292    });
2293
2294})(jQuery);

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.