PageSourceSearch

https://www.pacificoaks.edu/wp-content/plugins/tcs-client-nf-sync/…?ver=8e92a81dede5da3fe6ae341b6b26af54

js pacificoaks.edu collected 2026-10-02 07:46:38 UTC 132,803 bytes, 3,092 lines download raw bytes

1if (typeof nfForms === 'undefined') var nfForms = [];
2
3let autocompleteSetupInterval;
4
5jQuery(document).ready(() => {
6    if (jQuery('.rmf-gdrp')) jQuery('.rmf-gdrp').hide();
7    nfForms.forEach(form => {
8        const dateLimit = getDateLimit(form);
9        if (dateLimit > 0) limitDates(dateLimit);
10    });
11});
12
13// Add this debug function to check semester data
14function debugSemesterData() {
15    // console.log('ninjaFormDataJS.semestersHTML available:', typeof ninjaFormDataJS.semestersHTML !== 'undefined');
16    if (typeof ninjaFormDataJS.semestersHTML !== 'undefined') {
17        console.log('semestersHTML content length:', ninjaFormDataJS.semestersHTML.length);
18        console.log('semestersHTML sample:', ninjaFormDataJS.semestersHTML.substring(0, 200));
19    }
20}
21// End debug code
22
23// ==================== NEW: Program-Campus Mapping Helper Functions ====================
24
25/**
26 * Get the mapped campuses for a given program value using the program_campus_mapping
27 * from the page configuration.
28 * 
29 * @param {string} programValue - The program value to look up
30 * @param {Array} mappingData - The program_campus_mapping array from pageFieldConfig
31 * @returns {Array|null} Array of campus values mapped to the program, or null if not found
32 */
33function getMappedCampusesForProgram(programValue, mappingData) {
34    if (!mappingData || !Array.isArray(mappingData) || !programValue) {
35        return null;
36    }
37    
38    const mapping = mappingData.find(item => item.program === programValue);
39    return mapping && mapping.campuses ? mapping.campuses : null;
40}
41
42/**
43 * Get the program-campus mapping data from the page configuration.
44 * 
45 * @returns {Array|null} The program_campus_mapping array or null if not found
46 */
47function getProgramCampusMappingData() {
48    const pageConfig = ninjaFormDataJS.pageFieldConfig;
49    if (pageConfig && pageConfig.program_campus_mapping && Array.isArray(pageConfig.program_campus_mapping) && pageConfig.program_campus_mapping.length > 0) {
50        return pageConfig.program_campus_mapping;
51    }
52    return null;
53}
54
55/**
56 * Check if a program has campus mapping defined.
57 * 
58 * @param {string} programValue - The program value to check
59 * @param {Array} mappingData - The program_campus_mapping array
60 * @returns {boolean} True if the program has a mapping with at least one campus
61 */
62function hasProgramMapping(programValue, mappingData) {
63    if (!mappingData || !Array.isArray(mappingData) || !programValue) {
64        return false;
65    }
66    const mapping = mappingData.find(item => item.program === programValue);
67    return mapping && mapping.campuses && mapping.campuses.length > 0;
68}
69
70// ==================== END NEW FUNCTIONS ====================
71
72jQuery(document).on('nfFormReady', (e, layoutView) => {
73
74    // debugSemesterData();
75
76    const form = nfForms.find(f => layoutView.$el[0].id === `nf-form-${f.id}-cont`);
77    if (typeof form !== 'undefined') {
78        // Set global form context for rule engine
79        window.currentFormContext = form;
80        
81        // Initialize global field variables
82        schoolFieldVal = getSchoolSelectField(form);
83        schoolLabelVal = getSchoolLabelField(form);
84        campusFieldVal = getCampusSelectField(form);
85        campusLabelVal = getCampusLabelField(form);
86        campusDataFieldVal = getCampusDataField(form);
87        programFieldVal = getProgramSelectField(form);
88        programLabelVal = getProgramLabelField(form);
89        semesterFieldVal = getSemesterSelectField(form);
90        semesterLabelVal = getSemesterLabelField(form);
91        countryFieldIdVal = getCountryField(form);
92        stateFieldIdVal = getStateField(form);
93        
94        const submitButton = getSubmitButton(form);
95        const pageUrlField = getPageUrlField(form);
96        if (typeof pageUrlField !== 'undefined') captureURL(pageUrlField.id);
97        const thankYouPageField = getThankYouPageField(form);
98        if (typeof thankYouPageField !== 'undefined') setThankYouPage(thankYouPageField.id);
99        const srcParamField = getSrcParamTranslatedField(form);
100        if (typeof srcParamField !== 'undefined') getLeadSourceID(srcParamField.id);
101
102        // Fields for dropdowns
103        const schoolField = getSchoolSelectField(form);
104        const schoolLabel = getSchoolLabelField(form);
105        const campusField = getCampusSelectField(form);
106        const campusLabel = getCampusLabelField(form);
107        const campusDataField = getCampusDataField(form);
108        const programField = getProgramSelectField(form);
109        const programLabel = getProgramLabelField(form);
110        const semesterField = getSemesterSelectField(form);
111        const semesterLabel = getSemesterLabelField(form);
112        const countryFieldId = getCountryField(form)
113        const stateFieldId = getStateField(form)
114
115        // Determine the chaining order
116        const useDynamic = useDynamicCampusProgram(form);
117
118        // Handle forms with SCHOOL > PROGRAM > CAMPUS > SEMESTER chain (useDynamic === 'program')
119        console.log('schoolField', schoolField);
120        console.log('schoolLabel', schoolLabel);
121        console.log('campusField', campusField);
122        console.log('campusLabel', campusLabel);
123        console.log('programField', programField);
124        console.log('programLabel', programLabel);
125        console.log('semesterField', semesterField);
126        console.log('semesterLabel', semesterLabel);
127
128        if (typeof schoolField !== 'undefined' && typeof schoolLabel !== 'undefined' &&
129            typeof campusField !== 'undefined' && typeof campusLabel !== 'undefined' &&
130            typeof programField !== 'undefined' && typeof programLabel !== 'undefined' &&
131            typeof semesterField !== 'undefined' && typeof semesterLabel !== 'undefined') {
132
133            if (useDynamic === 'program') {
134                // Show School > Program > Campus > Semester
135                schoolProgramCampusSemesterChain(schoolField.id, schoolLabel.id, programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, countryFieldId, stateFieldId);
136            } else if (useDynamic === 'campus') {
137                // Show School > Campus > Program > Semester
138                schoolCampusProgramSemesterChain(schoolField.id, schoolLabel.id, programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, countryFieldId, stateFieldId);
139            } else {
140                // Non-chained dropdown behavior for school forms - load all options
141                nonChainedSchoolDropDowns(schoolField.id, schoolLabel.id, programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, countryFieldId, stateFieldId);
142            }
143
144        }
145        
146        // Handle forms with PROGRAM > CAMPUS > SEMESTER chain (no school) - useDynamic === 'program'
147        else if (typeof campusField !== 'undefined' && typeof campusLabel !== 'undefined' &&
148            typeof programField !== 'undefined' && typeof programLabel !== 'undefined' &&
149            typeof semesterField !== 'undefined' && typeof semesterLabel !== 'undefined') {
150
151            if (useDynamic === 'program') {
152                // Show program > campus > semester chain
153                programCampusSemesterChainNoSchool(programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, countryFieldId, stateFieldId);
154            } else if (useDynamic === 'campus') {
155                // Show campus > program > semester chain
156                campusProgramSemesterChainNoSchool(programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, countryFieldId, stateFieldId);
157            } else {
158                // Non-chained dropdown behavior
159                nonChainedDropDownsNoSchool(programField.id, programLabel.id, campusField.id, campusLabel.id, semesterField.id, semesterLabel.id, campusDataField, countryFieldId, stateFieldId);
160            }
161
162            const utmProgramField = getUtmProgramField(form);
163            const utmCampusField = getUtmCampusField(form);
164            if (typeof utmCampusField !== 'undefined' && typeof utmCampusField.value !== 'undefined') {
165                selectProgramCampusUTM(programField.id, programLabel.id, campusField.id, campusLabel.id, campusDataField, utmCampusField, utmProgramField, useDynamic);
166            }
167        }
168
169        // Handle forms with only PROGRAM (no campus/semester)
170        else if (typeof programField !== 'undefined' && typeof programLabel !== 'undefined') {
171            const utmProgramField = getUtmProgramField(form);
172            selectProgramUTM(programField.id, programLabel.id, utmProgramField);
173        }
174        // Handle forms with only SCHOOL (no program/campus)
175        else if (typeof schoolField !== 'undefined' && typeof schoolLabel !== 'undefined') {
176            initializeSchoolField(schoolField.id, schoolLabel.id);
177            const utmSchoolField = getUtmSchoolField(form);
178            if (typeof utmSchoolField !== 'undefined') {
179                selectSchoolUTM(schoolField.id, schoolLabel.id, utmSchoolField);
180            }
181        }
182
183        // Pre-select the "Campus of Interest" and "Program of Interest" saved
184        // in the page's TCS Field Config (backend), if set. Both dropdowns are
185        // still populated with their full option lists (see the chain
186        // functions above), so this only sets the *default* selection -- the
187        // visitor remains free to pick a different Campus afterwards, and the
188        // existing Campus change handler will refilter the Program list to
189        // match dynamically.
190        const pageConfigForDefaults = ninjaFormDataJS.pageFieldConfig;
191        if (pageConfigForDefaults && typeof campusField !== 'undefined' && typeof programField !== 'undefined') {
192            setTimeout(function () {
193                const $campusSelectEl = jQuery("#nf-field-" + campusField.id);
194                const $programSelectEl = jQuery("#nf-field-" + programField.id);
195                const defaultCampus = pageConfigForDefaults.campus;
196                const defaultProgram = pageConfigForDefaults.program_of_interest;
197
198                function selectSavedProgram() {
199                    if (defaultProgram && $programSelectEl.find('option[value="' + defaultProgram + '"]').length) {
200                        $programSelectEl.val(defaultProgram).trigger('change');
201                    }
202                }
203
204                if (useDynamic === 'campus' && defaultCampus && $campusSelectEl.find('option[value="' + defaultCampus + '"]').length) {
205                    // Campus-first chain: set the Campus first so its change
206                    // handler rebuilds the Program list scoped to it, then
207                    // apply the saved Program default from that filtered list.
208                    $campusSelectEl.val(defaultCampus).trigger('change');
209                    selectSavedProgram();
210                } else {
211                    // Program-first (or non-chained) forms: keep the original
212                    // behavior of selecting the Program first, then also apply
213                    // the saved Campus default, if present.
214                    selectSavedProgram();
215                    if (defaultCampus && $campusSelectEl.find('option[value="' + defaultCampus + '"]').length) {
216                        $campusSelectEl.val(defaultCampus).trigger('change');
217                    }
218                }
219            }, 0);
220        } else if (pageConfigForDefaults && pageConfigForDefaults.program_of_interest && typeof programField !== 'undefined') {
221            // Fallback for forms with a Program field but no Campus field.
222            setTimeout(function () {
223                const $programSelectEl = jQuery("#nf-field-" + programField.id);
224                if ($programSelectEl.find('option[value="' + pageConfigForDefaults.program_of_interest + '"]').length) {
225                    $programSelectEl.val(pageConfigForDefaults.program_of_interest).trigger('change');
226                }
227            }, 0);
228        }
229
230        // Rest of your existing code for other form fields...
231        const timeZoneField = getTimezoneField(form);
232        if (typeof timeZoneField !== 'undefined') bindChangesPartChange(timeZoneField.id, submitButton.id);
233        const countryField = getCountryField(form);
234        const countryZipTypeField = getCountryZipTypeField(form);
235        const zipStatusField = getZipStatusField(form);
236        const zipCodeField = getZipCodeField(form);
237        const postalField = getPostalCodeField(form);
238        const stateField = getStateField(form);
239        const provinceField = getProvinceField(form);
240        const intStateField = getInternationalStateField(form);
241        const cityField = getCityField(form);
242        if (typeof countryField !== 'undefined' && typeof countryZipTypeField !== 'undefined' &&
243            typeof zipStatusField !== 'undefined' && typeof zipCodeField !== 'undefined' &&
244            typeof postalField !== 'undefined' && typeof stateField !== 'undefined' &&
245            typeof intStateField !== 'undefined' && typeof cityField !== 'undefined') {
246
247            validateZipCode(countryField.id, countryZipTypeField.id, zipCodeField.id, postalField.id, stateField.id, provinceField.id, intStateField.id, zipStatusField.id, cityField.id, submitButton.id);
248            autocompleteSetupInterval = setInterval(() => {
249                setupAddressAutocomplete(countryField.id);
250            });
251        }
252        jQuery(`#nf-form-${form.id}-cont .nf-next`).click(() => {
253            const latField = getLocalLatField(form);
254            const longField = getLocalLongField(form);
255            if (typeof latField !== 'undefined' && typeof longField !== 'undefined') {
256                getLatLong(latField.id, longField.id, submitButton.id);
257            }
258            const countryField = getCountryField(form);
259            if (typeof countryField !== 'undefined') {
260                setupAddressAutocomplete(countryField.id);
261            }
262            if (typeof countryCodeField !== 'undefined') gdprRequirements(countryCodeField.id);
263        });
264        setEmailValidation();
265    }
266});
267
268// ==================== CHAINING FUNCTIONS ====================
269// Helper function to initialize dropdown with default option
270function initializeDropdownWithDefault(fieldID, defaultText) {
271    const $field = jQuery("#nf-field-" + fieldID);
272    if ($field.length) {
273        $field.find('option').remove();
274        $field.find('optgroup').remove();
275        $field.append(`<option value="" label="">${defaultText || '--'}</option>`);
276    }
277}
278
279// Auto-select a dropdown's only real option (i.e. excluding the blank/placeholder
280// option) so the user isn't forced to manually pick a value when there is only one
281// available choice. Used for Program, Campus, and Semester fields.
282function autoSelectSingleOption(fieldID) {
283    if (!fieldID) return false;
284    const $select = jQuery("#nf-field-" + fieldID);
285    if (!$select.length) return false;
286
287    const optionsWithValue = $select.find("option").filter(function () {
288        const val = jQuery(this).val();
289        return val && val.trim() !== "";
290    });
291
292    if (optionsWithValue.length !== 1) return false;
293
294    const onlyValue = optionsWithValue.eq(0).val();
295    if ($select.val() === onlyValue) return false; // already selected, avoid re-triggering
296
297    optionsWithValue.prop("selected", true);
298    $select.trigger('change');
299    return true;
300}
301
302// ==================== UPDATED: Filter campuses by program using mapping or data-class fallback ====================
303
304/**
305 * Filter campus dropdown options based on the selected program.
306 * First tries to use program_campus_mapping from page config.
307 * If no mapping found for the program, falls back to data-class filtering.
308 * 
309 * @param {string} programValue - The selected program value
310 * @param {string} campusID - The ID of the campus dropdown field
311 * @param {string} programID - The ID of the program dropdown field (needed for data-class fallback)
312 * @returns {boolean} True if mapping was applied, false if using data-class fallback
313 */
314function filterCampusesByProgramMapping(programValue, campusID, programID) {
315    const mappingData = getProgramCampusMappingData();
316    
317    // Check if mapping exists for this specific program
318    if (mappingData && programValue) {
319        const mappedCampuses = getMappedCampusesForProgram(programValue, mappingData);
320        
321        // If mapping exists for this program, use it
322        if (mappedCampuses !== null && mappedCampuses.length > 0) {
323            // Get the campus dropdown element
324            const $campusSelect = jQuery("#nf-field-" + campusID);
325            
326            // Filter campus options based on mapping
327            $campusSelect.find('option').each(function() {
328                const $option = jQuery(this);
329                const optionValue = $option.val();
330                
331                // Skip the default/placeholder option
332                if (optionValue === '') return;
333                
334                // Check if this campus is in the mapped campuses list
335                const shouldShow = mappedCampuses.includes(optionValue);
336                
337                if (shouldShow) {
338                    $option.show();
339                    $option.prop('disabled', false);
340                } else {
341                    $option.hide();
342                    if ($option.is(':selected')) {
343                        $option.prop('selected', false);
344                    }
345                    $option.prop('disabled', true);
346                }
347            });
348            
349            // After filtering, check if only one campus remains and auto-select it
350            const visibleOptions = $campusSelect.find('option').filter(function() {
351                return jQuery(this).val() !== '' && jQuery(this).is(':visible');
352            });
353            
354            if (visibleOptions.length === 1) {
355                visibleOptions.prop('selected', true);
356                $campusSelect.trigger('change');
357            }
358            
359            return true; // Mapping was applied
360        }
361    }
362    
363    // No mapping found for this program - FALL BACK TO ORIGINAL data-class filtering
364    if (programValue && programID) {
365        // Get the selected program option to read its data-class attribute
366        const $selectedProgram = jQuery("#nf-field-" + programID + " option:selected");
367        let optionCampuses = $selectedProgram.attr('data-class');
368        
369        if (typeof optionCampuses !== 'undefined' && optionCampuses) {
370            optionCampuses = optionCampuses.split(' ');
371            const $campusSelect = jQuery("#nf-field-" + campusID);
372            
373            $campusSelect.find('option').each(function() {
374                const $option = jQuery(this);
375                const optionValue = $option.val();
376                
377                // Skip the default/placeholder option
378                if (optionValue === '') return;
379                
380                // Check if this campus is in the data-class list
381                const shouldShow = optionCampuses.indexOf(optionValue) !== -1;
382                
383                if (shouldShow) {
384                    $option.show();
385                    $option.prop('disabled', false);
386                } else {
387                    $option.hide();
388                    if ($option.is(':selected')) {
389                        $option.prop('selected', false);
390                    }
391                    $option.prop('disabled', true);
392                }
393            });
394            
395            // After filtering, check if only one campus remains and auto-select it
396            const visibleOptions = $campusSelect.find('option').filter(function() {
397                return jQuery(this).val() !== '' && jQuery(this).is(':visible');
398            });
399            
400            if (visibleOptions.length === 1) {
401                visibleOptions.prop('selected', true);
402                $campusSelect.trigger('change');
403            }
404            
405            return false; // Using data-class fallback
406        }
407    }
408    
409    // If no filtering method works, show all campuses
410    const $campusSelect = jQuery("#nf-field-" + campusID);
411    $campusSelect.find('option').show();
412    $campusSelect.find('option').prop('disabled', false);
413    
414    return false;
415}
416
417/**
418 * Filter programs based on selected campus using program_campus_mapping.
419 * Used in campus-first chains. Falls back to original behavior if no mapping.
420 * 
421 * @param {string} campusValue - The selected campus value
422 * @param {string} programID - The ID of the program dropdown field
423 * @returns {boolean} True if mapping was applied, false if using fallback
424 */
425function filterProgramsByCampusMapping(campusValue, programID) {
426    const mappingData = getProgramCampusMappingData();
427    const $programSelect = jQuery("#nf-field-" + programID);
428    let hasMappingForAnyProgram = false;
429    
430    // First, check if any program has mapping defined
431    if (mappingData) {
432        $programSelect.find('option').each(function() {
433            const $option = jQuery(this);
434            const optionValue = $option.val();
435            
436            // Skip the default/placeholder option
437            if (optionValue === '') return;
438            
439            // Get mapped campuses for this program
440            const mappedCampuses = getMappedCampusesForProgram(optionValue, mappingData);
441            
442            // If this program has mapping defined
443            if (mappedCampuses !== null && mappedCampuses.length > 0) {
444                hasMappingForAnyProgram = true;
445                // Show only if mapping includes the selected campus
446                const shouldShow = mappedCampuses.includes(campusValue);
447                if (shouldShow) {
448                    $option.show();
449                    $option.prop('disabled', false);
450                } else {
451                    $option.hide();
452                    if ($option.is(':selected')) {
453                        $option.prop('selected', false);
454                    }
455                    $option.prop('disabled', true);
456                }
457            }
458        });
459    }
460    
461    // If no programs have mapping defined, fall back to original data-class filtering
462    if (!hasMappingForAnyProgram) {
463        // Reset all options to visible first
464        $programSelect.find('option').show();
465        $programSelect.find('option').prop('disabled', false);
466        
467        // Now apply data-class filtering
468        $programSelect.find('option').each(function() {
469            const $option = jQuery(this);
470            const optionValue = $option.val();
471            
472            // Skip the default/placeholder option
473            if (optionValue === '') return;
474            
475            // Get the data-class attribute of this program option
476            const optionCampuses = ($option.attr('data-class') || '').split(' ').filter(
476Boolean);
477            
478            // If the program has data-class and the selected campus is in it, keep it
479            // If the program has no data-class (empty), show it (fallback)
480            if (optionCampuses.length > 0) {
481                const shouldShow = optionCampuses.indexOf(campusValue) !== -1;
482                if (!shouldShow) {
483                    $option.hide();
484                    if ($option.is(':selected')) {
485                        $option.prop('selected', false);
486                    }
487                    $option.prop('disabled', true);
488                }
489            }
490            // If no data-class, keep it visible (fallback)
491        });
492        
493        return false;
494    }
495    
496    return true;
497}
498
499// ===========================================================================================================================
500
501//  UNIVERSAL RULE PROCESSOR (Supports "if", AND/OR after-if, before-if definitions)
502
503window.NF_DO_NOT_SUBMIT = false;
504window.NF_DO_NOT_SUBMIT_MSG = '';
505window.currentFormContext = null; // Store current form context
506
507// These will be set when form is ready
508let schoolFieldVal, schoolLabelVal, campusFieldVal, campusLabelVal, campusDataFieldVal;
509let programFieldVal, programLabelVal, semesterFieldVal, semesterLabelVal;
510let countryFieldIdVal, stateFieldIdVal;
511
512function enforceDoNotSubmit() {
513    if (!window.currentFormContext) return false;
514    
515    const form = window.currentFormContext;
516    const submitBtnId = getSubmitButton(form)?.id;
517    if (!submitBtnId) return false;
518
519    const $submitBtn = jQuery('#nf-field-' + submitBtnId);
520
521    if (!$submitBtn.length) return false;
522
523    if (window.NF_DO_NOT_SUBMIT) {
524        $submitBtn.prop('disabled', true).attr('aria-disabled', 'true').addClass('nf-disabled');
525        showDO_NOT_SUBMIT_error(submitBtnId, window.NF_DO_NOT_SUBMIT_MSG);
526    } else {
527        $submitBtn.prop('disabled', false).removeAttr('aria-disabled').removeClass('nf-disabled');
528        hideDO_NOT_SUBMIT_error(submitBtnId);
529    }
530
531    return true;
532}
533
534function runRuleEngineAfterChange() {
535    console.log('runRuleEngineAfterChange');
536
537    const values = {
538        program: sessionStorage.getItem("nf_program") || jQuery("#nf-field-" + programFieldVal?.id)?.val(),
539        campus: sessionStorage.getItem("nf_campus") || jQuery("#nf-field-" + campusFieldVal?.id)?.val(),
540        semester: sessionStorage.getItem("nf_semester") || jQuery("#nf-field-" + semesterFieldVal?.id)?.val(),
541        country: sessionStorage.getItem("nf_country") || jQuery("#nf-field-" + countryFieldIdVal?.id)?.val(),
542        state: sessionStorage.getItem("nf_state") || jQuery("#nf-field-" + stateFieldIdVal?.id)?.val(),
543    };
544
545    const fieldIDs = {
546        program: programFieldVal?.id,
547        campus: campusFieldVal?.id,
548        semester: semesterFieldVal?.id,
549        country: countryFieldIdVal?.id,
550        state: stateFieldIdVal?.id
551    };
552
553    // STEP 1: RESET everything first
554    resetRuleEffects(fieldIDs);
555
556    // STEP 2: Apply rules fresh
557    processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
558}
559
560var DoNotSubmitController = Backbone.Model.extend({
561    initialize: function () {
562        // Step change (multi-part)
563        this.listenTo(nfRadio.channel('nfMP'), 'change:part', this.enforce);
564
565        // Submit button rendered / re-rendered
566        this.listenTo(nfRadio.channel('forms'), 'submit:ready', this.enforce);
567    },
568
569    enforce: function () {
570        // Set up field change handlers once
571        if (countryFieldIdVal?.id) {
572            jQuery("#nf-field-" + countryFieldIdVal.id).off('change').on('change', function () {
573                runRuleEngineAfterChange();
574                console.log('country change')
575            });
576        }
577        
578        if (stateFieldIdVal?.id) {
579            jQuery("#nf-field-" + stateFieldIdVal.id).off('change').on('change', function () {
580                runRuleEngineAfterChange();
581                console.log('state change')
582            });
583        }
584
585        // Run rule engine once
586        runRuleEngineAfterChange();
587    }
588});
589
590jQuery(document).ready(function () {
591    if (typeof nfRadio !== 'undefined') {
592        console.log("Page Change")
593        new DoNotSubmitController();
594    }
595});
596
597jQuery(document).on('nfFormReady', (e, layoutView) => {
598    // Run once when form is ready
599    setTimeout(enforceDoNotSubmit, 300);
600
601    // Re-check every time ANY field changes value
602    Backbone.Radio.channel('fields').on('change:modelValue', function (model) {
603        enforceDoNotSubmit();
604    });
605
606    // Re-check on part (page) change — very common source of re-enabling
607    const mpChannel = Backbone.Radio.channel('nfMP');
608    mpChannel.on('change:part', function () {
609        setTimeout(enforceDoNotSubmit, 150);
610    });
611
612    // Also useful in many Ninja Forms setups:
613    jQuery(document).on('change', '.ninja-forms-field', function () {
614        setTimeout(enforceDoNotSubmit, 100);
615    });
616
617    // Bonus: after validation run
618    Backbone.Radio.channel('submit').on('validate:field', function () {
619        setTimeout(enforceDoNotSubmit, 120);
620    });
621});
622
623function beforeIFCompleted(beforeIF, values) {
624    console.log('beforeIF', beforeIF)
625    console.log('values', values)
626    if (!beforeIF.length) return false;
627
628    let status = false;
629    let first = true;
630
631    beforeIF.forEach(act => {
632
633        const valueMatch = (values[act.rule_sub_field] == act.rule_sub_value);
634
635        // FIRST ITEM (do-not-submit block)
636        if (first) {
637            status = valueMatch;
638            first = false;
639            return;
640        }
641
642        // OR logic
643        if (act.rule_sub_function === "or") {
644            status = status || valueMatch;
645        }
646
647        // AND logic
648        if (act.rule_sub_function === "and") {
649            status = status && valueMatch;
650        }
651    });
652
653    return status;
654}
655
656function processRules(rules, values, fieldIDs) {
657    console.log('Rules', rules);
658    console.log('values', values);
659    console.log('fieldIDs', fieldIDs);
660
661    Object.keys(rules).forEach(ruleId => {
662
663        const rule = rules[ruleId];
664        const nested = rule.rule_nested_function || [];
665
666        let beforeIF = [];
667        let afterIF = [];
668        let mainIF = null;
669        let foundIF = false;
670
671        // Always push main action (hide/show)
672        beforeIF.push({
673            rule_sub_function: rule.rule_function,
674            rule_sub_field: rule.rule_field,
675            rule_sub_value: rule.rule_value
676        });
677
678        // SPLIT: beforeIF / IF / afterIF
679        nested.forEach(item => {
680            if (item.rule_sub_function === "if") {
681                foundIF = true;
682                mainIF = item;
683                afterIF.push(item); // IF stays inside afterIF group
684            } else if (!foundIF) {
685                beforeIF.push(item); // multiple hide actions here
686            } else {
687                afterIF.push(item); // OR & AND go here
688            }
689        });
690
691        if (!mainIF) return;
692
693        let status = false; // default OR-logic safe start
694        let first = true;
695
696        afterIF.forEach(item => {
697
698            if (item.rule_sub_function === "if") {
699                status = (values[item.rule_sub_field] == item.rule_sub_value);
700                first = false;
701            }
702
703            if (item.rule_sub_function === "or") {
704                status = status || (values[item.rule_sub_field] == item.rule_sub_value);
705            }
706
707            if (item.rule_sub_function === "and") {
708                status = status && (values[item.rule_sub_field] == item.rule_sub_value);
709            }
710
711        });
712
713        // APPLY FINAL RESULT TO ALL hide actions
714        if (status && rule.rule_function === "hide") {
715            console.log('hide');
716            beforeIF.forEach(act => {
717                hideField(fieldIDs[act.rule_sub_field], act.rule_sub_value);
718            });
719        }
720        else if (status && rule.rule_function === "do-not-submit") {
721            console.log('do not submit');
722
723            // Check: BeforeIF fields also must be selected
724            const beforeDone = beforeIFCompleted(beforeIF, values);
725            console.log('beforeDone', beforeDone);
726
727            if (beforeDone) {
728                console.log('iff do not submit');
729                const form = window.currentFormContext;
730                if (!form) return;
731                const submitBtnId = getSubmitButton(form).id;
732                const $submitBtn = jQuery('#nf-field-' + submitBtnId);
733
734                window.NF_DO_NOT_SUBMIT = true;
735                window.NF_DO_NOT_SUBMIT_MSG = rule?.rule_dns_msg || "You cannot submit this form.";
736
737                $submitBtn.prop('disabled', true).addClass('nf-disabled');
738
739                const errorMsg = rule.rule_dns_msg || "You cannot submit this form.";
740                showDO_NOT_SUBMIT_error(submitBtnId, errorMsg);
741
742            } else {
743                console.log('else do not submit');
744                // Before fields NOT completed → keep button enabled
745                const form = window.currentFormContext;
746                if (!form) return;
747                const submitBtnId = getSubmitButton(form).id;
748                const $submitBtn = jQuery('#nf-field-' + submitBtnId);
749
750                window.NF_DO_NOT_SUBMIT = false;
751                window.NF_DO_NOT_SUBMIT_MSG = "";
752                $submitBtn.prop('disabled', false).removeClass('nf-disabled');
753                hideDO_NOT_SUBMIT_error(submitBtnId);
754            }
755        }
756
757    });
758}
759
760function showDO_NOT_SUBMIT_error(fieldID, message) {
761    let $field = jQuery('#nf-field-' + fieldID);
762    let $container = $field.closest('.submit-container');
763
764    // Remove old message if exists
765    $container.find('.rule-error').remove();
766
767    // Add new message
768    $container.append(
769        `<div class="rule-error" style="color:#c00; margin-top:5px; font-size:13px;">${message}</div>`
770    );
771}
772
773function hideDO_NOT_SUBMIT_error(fieldID) {
774    let $field = jQuery('#nf-field-' + fieldID);
775    let $container = $field.closest('.submit-container');
776
777    // Remove old message if exists
778    $container.find('.rule-error').remove();
779}
780
781function hideField(fieldId, fieldValue) {
782    const $el = jQuery('#nf-field-' + fieldId);
783
784    const oldVal = $el.val();
785
786    // Remove option
787    const removed = $el.find('option[value="' + fieldValue + '"]');
788    if (!removed.length) return; // nothing to do
789
790    // removed.remove();
791    // removed.hide();
792    removed.prop('disabled', true);
793
794    // If the removed value was selected → reset value & trigger change
795    if (oldVal == fieldValue) {
796        $el.val('');
797        $el.trigger('change');
798        $el.find('option[value="' + fieldValue + '"]').prop('disabled', true);
799    }
800}
801
802function showField(fieldName) {
803    const el = jQuery('#nf-field-' + fieldName);
804    el.find('option').show();  // show all again
805    el.find('option').prop('disabled', false);
806}
807
808function resetRuleEffects(fieldIDs) {
809    // Re-enable all options in all rule fields
810    Object.values(fieldIDs).forEach(fieldId => {
811        const $el = jQuery('#nf-field-' + fieldId);
812        if ($el.length) {
813            $el.find('option').prop('disabled', false);
814        }
815    });
816
817    // Enable submit button
818    if (window.currentFormContext) {
819        const submitBtnId = getSubmitButton(window.currentFormContext).id;
820        const $submitBtn = jQuery('#nf-field-' + submitBtnId);
821        $submitBtn.prop('disabled', false).removeClass('nf-disabled');
822        hideDO_NOT_SUBMIT_error(submitBtnId);
823    }
824}
825
826// ===========================================================================================================================
827
828// SCHOOL > PROGRAM > CAMPUS > SEMESTER chain (useDynamic === 'program')
829function schoolProgramCampusSemesterChain(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
830    // Initialize all dropdowns with appropriate default options
831    initializeDropdownWithDefault(schoolID, 'Choose School');
832    initializeDropdownWithDefault(programID, '--');
833    initializeDropdownWithDefault(campusID, '--');
834    initializeDropdownWithDefault(semesterID, '--');
835
836    // Initialize school field (no default option needed as it should show all schools)
837    loadSchoolOptions(schoolID, schoolLabelID);
838
839    // School -> Program filtering
840    jQuery("#nf-field-" + schoolID).change(function () {
841        const schoolValue = jQuery(this).val();
842
843        // Reset program dropdown with default option
844        initializeDropdownWithDefault(programID, 'Choose Program');
845
846        if (schoolValue) {
847            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
848
849            // Filter programs based on selected school - USING data-school-id
850            jQuery("#nf-field-" + programID + " option").each((index, option) => {
851                let optionSchoolId = jQuery(option).attr('data-school-id');
852
853                if (typeof optionSchoolId !== 'undefined' && schoolValue !== '' && option.value !== '') {
854                    // Compare the school ID directly
855                    if (optionSchoolId !== schoolValue) {
856                        jQuery(option).remove();
857                    }
858                }
859            });
860
861            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
862                if (jQuery(option).is(':empty')) {
863                    jQuery(option).remove();
864                }
865            });
866        }
867
868        // Reset dependent fields to default (Campus and Semester)
869        initializeDropdownWithDefault(campusID, '--');
870        initializeDropdownWithDefault(semesterID, '--');
871
872        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
873        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
874
875        // If only one Program remains available, auto-select it instead of
876        // requiring the user to choose it manually.
877        autoSelectSingleOption(programID);
878
879        runRuleEngine();
880    });
881
882    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
883    jQuery("#nf-field-" + programID).change(function () {
884        const programValue = jQuery(this).val();
885
886        // Reset campus dropdown with default option
887        initializeDropdownWithDefault(campusID, 'Choose Campus');
888
889        if (programValue) {
890            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
891            
892            // Try to filter using mapping, fallback to data-class if no mapping
893            filterCampusesByProgramMapping(programValue, campusID, programID);
894
895            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
896                if (jQuery(option).is(':empty')) {
897                    jQuery(option).remove();
898                }
899            });
900        }
901
902        // Reset dependent field (Semester) to default
903        initializeDropdownWithDefault(semesterID, '--');
904
905        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
906        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
907
908        // If only one Campus remains available, auto-select it instead of
909        // requiring the user to choose it manually.
910        autoSelectSingleOption(campusID);
911
912        runRuleEngine();
913    });
914
915    // Campus -> Semester filtering - FIXED VERSION
916    jQuery("#nf-field-" + campusID).change(function () {
917        const campusValue = jQuery(this).val();
918        const programValue = jQuery("#nf-field-" + programID).val(); // Get current program value
919
920        // Reset semester dropdown with default option
921        initializeDropdownWithDefault(semesterID, 'Choose Semester');
922
923        if (campusValue && programValue) {
924            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
925
926            // Filter semesters based on selected program AND campus
927            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
928                let optionSemesters = jQuery(option).attr('data-class');
929
930                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
931                    // The data-class contains pairs like "67-104 67-802"
932                    // We need to check if any pair matches the selected program and campus
933                    const semesterPairs = optionSemesters.split(' ');
934                    let shouldKeep = false;
935
936                    semesterPairs.forEach(pair => {
937                        const [programId, campusId] = pair.split('-');
938                        if (programId === programValue && campusId === campusValue) {
939                            shouldKeep = true;
940                        }
941                    });
942
943                    if (!shouldKeep && option.value !== '') {
944                        jQuery(option).remove();
945                    }
946                }
947            });
948
949            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
950                if (jQuery(option).is(':empty')) {
951                    jQuery(option).remove();
952                }
953            });
954
955            singleSemesterSelect(semesterID, campusID);
956        }
957
958        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
959        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
960        runRuleEngine();
961    });
962
963    // Semester change handler
964    jQuery("#nf-field-" + semesterID).change(function () {
965        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
966        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
967        runRuleEngine();
968    });
969
970    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
971        runRuleEngine();
972    })
973    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
974        runRuleEngine();
975    })
976
977    function runRuleEngine() {
978
979        const values = {
980            school: jQuery("#nf-field-" + schoolID).val(),
981            program: jQuery("#nf-field-" + programID).val(),
982            campus: jQuery("#nf-field-" + campusID).val(),
983            semester: jQuery("#nf-field-" + semesterID).val(),
984            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
985            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
986        };
987
988        const fieldIDs = {
989            school: schoolID,
990            program: programID,
991            campus: campusID,
992            semester: semesterID,
993            country: countryFieldId?.id,
994            state: stateFieldId?.id
995        };
996
997        // STEP 1: RESET everything first
998        resetRuleEffects(fieldIDs);
999
1000        // STEP 2: Apply rules fresh
1001        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1002    }
1003
1004    runRuleEngine(); // run once on load
1005}
1006
1007// =========================================================================================================================================================================================
1008
1009// SCHOOL > CAMPUS > PROGRAM > SEMESTER chain (useDynamic === 'campus')
1010function schoolCampusProgramSemesterChain(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1011    // Initialize all dropdowns with appropriate default options
1012    initializeDropdownWithDefault(schoolID, 'Choose School');
1013    initializeDropdownWithDefault(programID, '--');
1014    initializeDropdownWithDefault(campusID, '--');
1015    initializeDropdownWithDefault(semesterID, '--');
1016
1017    // Initialize school field
1018    loadSchoolOptions(schoolID, schoolLabelID);
1019
1020    // School -> Campus filtering
1021    jQuery("#nf-field-" + schoolID).change(function () {
1022        const schoolValue = jQuery(this).val();
1023
1024        // Reset campus dropdown with default option
1025        initializeDropdownWithDefault(campusID, 'Choose Campus');
1026
1027        if (schoolValue) {
1028            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1029
1030            // Filter campuses based on selected school - USING data-school-id
1031            jQuery("#nf-field-" + campusID + " option").each((index, option) => {
1032                let optionSchoolId = jQuery(option).attr('data-school-id');
1033
1034                if (typeof optionSchoolId !== 'undefined' && schoolValue !== '' && option.value !== '') {
1035                    // Compare the school ID directly
1036                    if (optionSchoolId !== schoolValue) {
1037                        jQuery(option).remove();
1038                    }
1039                }
1040            });
1041
1042            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1043                if (jQuery(option).is(':empty')) {
1044                    jQuery(option).remove();
1045                }
1046            });
1047        }
1048
1049        // Reset dependent fields to default (Program and Semester)
1050        initializeDropdownWithDefault(programID, '--');
1051        initializeDropdownWithDefault(semesterID, '--');
1052
1053        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1054        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1055
1056        // If only one Campus remains available, auto-select it instead of
1057        // requiring the user to choose it manually.
1058        autoSelectSingleOption(campusID);
1059
1060        runRuleEngine();
1061    });
1062
1063    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1064    jQuery("#nf-field-" + campusID).change(function () {
1065        const campusValue = jQuery(this).val();
1066
1067        // Reset program dropdown with default option
1068        initializeDropdownWithDefault(programID, 'Choose Program');
1069
1070        if (campusValue) {
1071            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1072            
1073            // Try to filter programs using mapping
1074            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1075            
1076            // If no mapping was applied, show all programs
1077            if (!mappingApplied) {
1078                // Filter programs based on selected campus
1079                jQuery("#nf-field-" + programID + " option").each((index, option) => {
1080                    let optionCampuses = jQuery(option).attr('data-class');
1081                    if (typeof optionCampuses !== 'undefined' && campusValue !== '') {
1082                        optionCampuses = optionCampuses.split(' ');
1083                        if (optionCampuses.indexOf(campusValue) === -1 && option.value !== '') {
1084                            jQuery(option).remove();
1085                        }
1086                    }
1087                });
1088            }
1089
1090            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1091                if (jQuery(option).is(':empty')) {
1092                    jQuery(option).remove();
1093                }
1094            });
1095        }
1096
1097        // Reset dependent field (Semester) to default
1098        initializeDropdownWithDefault(semesterID, '--');
1099
1100        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1101        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1102
1103        // If only one Program remains available, auto-select it instead of
1104        // requiring the user to choose it manually.
1105        autoSelectSingleOption(programID);
1106
1107        runRuleEngine();
1108    });
1109
1110    // Program -> Semester filtering
1111    jQuery("#nf-field-" + programID).change(function () {
1112        const programValue = jQuery(this).val();
1113        const campusValue = jQuery("#nf-field-" + campusID).val();
1114
1115        // Reset semester dropdown with default option
1116        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1117
1118        if (programValue && campusValue) {
1119            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1120
1121            // Filter semesters based on selected program AND campus
1122            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1123                let optionSemesters = jQuery(option).attr('data-class');
1124                let optionPrograms = jQuery(option).attr('data-program-solo-id');
1125
1126                let shouldRemove = false;
1127
1128                // Filter by campus and program using data-class (format: "program-campus" pairs)
1129                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1130                    const semesterPairs = optionSemesters.split(' ');
1131                    let hasMatchingPair = false;
1132
1133                    semesterPairs.forEach(pair => {
1134                        const [programId, campusId] = pair.split('-');
1135                        if (programId === programValue && campusId === campusValue) {
1136                            hasMatchingPair = true;
1137                        }
1138                    });
1139
1140                    if (!hasMatchingPair && option.value !== '') {
1141                        shouldRemove = true;
1142                    }
1143                }
1144
1145                // Additional filter by program using data-program-solo-id
1146                if (!shouldRemove && typeof optionPrograms !== 'undefined' && programValue !== '') {
1147                    optionPrograms = optionPrograms.split(' ');
1148                    if (optionPrograms.indexOf(programValue) === -1 && option.value !== '') {
1149                        shouldRemove = true;
1150                    }
1151                }
1152
1153                if (shouldRemove) {
1154                    jQuery(option).remove();
1155                }
1156            });
1157
1158            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1159                if (jQuery(option).is(':empty')) {
1160                    jQuery(option).remove();
1161                }
1162            });
1163
1164            singleSemesterSelect(semesterID, campusID);
1165        }
1166
1167        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1168        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1169        runRuleEngine();
1170    });
1171
1172    // Semester change handler
1173    jQuery("#nf-field-" + semesterID).change(function () {
1174        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1175        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1176        runRuleEngine();
1177    });
1178
1179    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1180        runRuleEngine();
1181    })
1182    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1183        runRuleEngine();
1184    })
1185
1186    function runRuleEngine() {
1187
1188        const values = {
1189            school: jQuery("#nf-field-" + schoolID).val(),
1190            program: jQuery("#nf-field-" + programID).val(),
1191            campus: jQuery("#nf-field-" + campusID).val(),
1192            semester: jQuery("#nf-field-" + semesterID).val(),
1193            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1194            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1195        };
1196
1197        const fieldIDs = {
1198            school: schoolID,
1199            program: programID,
1200            campus: campusID,
1201            semester: semesterID,
1202            country: countryFieldId?.id,
1203            state: stateFieldId?.id
1204        };
1205
1206        // STEP 1: RESET everything first
1207        resetRuleEffects(fieldIDs);
1208
1209        // STEP 2: Apply rules fresh
1210        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1211    }
1212
1213    runRuleEngine(); // run once on load
1214}
1215
1216// PROGRAM > CAMPUS > SEMESTER chain (no school, useDynamic === 'program') - UPDATED
1217function programCampusSemesterChainNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1218    console.log('programCampusSemesterChainNoSchool');
1219
1220    // Initialize program field with all options
1221    const $programSelect = jQuery('#nf-field-' + programID);
1222    $programSelect.find('option').remove();
1223    $programSelect.find('optgroup').remove();
1224    $programSelect.append('<option value="" label="">Choose Program</option>');
1225    $programSelect.append(ninjaFormDataJS.programsHTML);
1226
1227    // Initialize other fields with default options
1228    initializeDropdownWithDefault(campusID, '--');
1229    initializeDropdownWithDefault(semesterID, '--');
1230
1231    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
1232    jQuery("#nf-field-" + programID).change(function () {
1233        const programValue = jQuery(this).val();
1234
1235        // Reset campus dropdown with default option
1236        initializeDropdownWithDefault(campusID, 'Choose Campus');
1237        // Reset semester dropdown with default option
1238        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1239
1240        if (programValue) {
1241            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1242
1243            // Try to filter using mapping, fallback to data-class if no mapping
1244            filterCampusesByProgramMapping(programValue, campusID, programID);
1245
1246            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1247                if (jQuery(option).is(':empty')) {
1248                    jQuery(option).remove();
1249                }
1250            });
1251
1252            // Populate Semester filtered by the selected Program alone
1253            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1254
1255            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1256                if (option.value === '') return;
1257                const optionPrograms = (jQuery(option).attr('data-program-solo-id') || '').split(' ').filter(
1257Boolean);
1258                if (optionPrograms.indexOf(programValue) === -1) {
1259                    jQuery(option).remove();
1260                }
1261            });
1262
1263            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1264                if (jQuery(option).is(':empty')) {
1265                    jQuery(option).remove();
1266                }
1267            });
1268        }
1269
1270        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1271        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1272
1273        // If only one Campus remains available, auto-select it instead of
1274        // requiring the user to choose it manually.
1275        autoSelectSingleOption(campusID);
1276
1277        runRuleEngine();
1278    });
1279
1280    // Campus -> Semester filtering
1281    jQuery("#nf-field-" + campusID).change(function () {
1282        const campusValue = jQuery(this).val();
1283        const programValue = jQuery("#nf-field-" + programID).val(); // Get current program value
1284
1285        // Reset semester dropdown with default option
1286        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1287
1288        if (campusValue && programValue) {
1289            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1290
1291            // Filter semesters based on selected campus and program
1292            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1293                let optionSemesters = jQuery(option).attr('data-class');
1294
1295                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1296                    // The data-class contains pairs like "67-104 67-802"
1297                    // We need to check if any pair matches the selected program and campus
1298                    const semesterPairs = optionSemesters.split(' ');
1299                    let shouldKeep = false;
1300
1301                    semesterPairs.forEach(pair => {
1302                        const [programId, campusId] = pair.split('-');
1303                        if (programId === programValue && campusId === campusValue) {
1304                            shouldKeep = true;
1305                        }
1306                    });
1307
1308                    if (!shouldKeep && option.value !== '') {
1309                        jQuery(option).remove();
1310                    }
1311                }
1312            });
1313
1314            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1315                if (jQuery(option).is(':empty')) {
1316                    jQuery(option).remove();
1317                }
1318            });
1319
1320            singleSemesterSelect(semesterID, campusID);
1321        }
1322
1323        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1324        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1325        runRuleEngine();
1326    });
1327
1328    // Semester change handler
1329    jQuery("#nf-field-" + semesterID).change(function () {
1330        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1331        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1332        runRuleEngine();
1333    });
1334
1335    // Program label change handler
1336    jQuery("#nf-field-" + programID).change(function () {
1337        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1338        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1339        runRuleEngine();
1340    });
1341
1342    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1343        runRuleEngine();
1344    })
1345    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1346        runRuleEngine();
1347    })
1348
1349    function runRuleEngine() {
1350        console.log('runRuleEngine');
1351
1352        const values = {
1353            program: jQuery("#nf-field-" + programID).val(),
1354            campus: jQuery("#nf-field-" + campusID).val(),
1355            semester: jQuery("#nf-field-" + semesterID).val(),
1356            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1357            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1358        };
1359
1360        sessionStorage.setItem("nf_program", values.program || "");
1361        sessionStorage.setItem("nf_campus", values.campus || "");
1362        sessionStorage.setItem("nf_semester", values.semester || "");
1363        sessionStorage.setItem("nf_country", values.country || "");
1364        sessionStorage.setItem("nf_state", values.state || "");
1365
1366        const fieldIDs = {
1367            program: programID,
1368            campus: campusID,
1369            semester: semesterID,
1370            country: countryFieldId?.id,
1371            state: stateFieldId?.id
1372        };
1373
1374        // STEP 1: RESET everything first
1375        resetRuleEffects(fieldIDs);
1376
1377        // STEP 2: Apply rules fresh
1378        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1379    }
1380
1381    // If only one Program is available on load, auto-select it. This cascades
1382    // (via the change handlers above) to auto-select Campus and Semester too,
1383    // when each of those also only has a single available option.
1384    autoSelectSingleOption(programID);
1385
1386    runRuleEngine(); // run once on load
1387}
1388
1389// Non-chained dropdown behavior (no school, useDynamic === '')
1390// CAMPUS > PROGRAM > SEMESTER chain (no school, useDynamic === 'campus') - UPDATED
1391function campusProgramSemesterChainNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1392    // Initialize campus field with "Choose Campus" on page load
1393    const $campusSelect = jQuery('#nf-field-' + campusID);
1394    $campusSelect.find('option').remove();
1395    $campusSelect.find('optgroup').remove();
1396    $campusSelect.append('<option value="" label="">Choose Campus</option>');
1397    $campusSelect.append(ninjaFormDataJS.campusesHTML);
1398
1399    // Initialize program field with "--" on page load
1400    initializeDropdownWithDefault(programID, '--');
1401
1402    // Initialize semester field with "--" on page load
1403    initializeDropdownWithDefault(semesterID, '--');
1404
1405    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1406    jQuery("#nf-field-" + campusID).change(function () {
1407        const campusValue = jQuery(this).val();
1408
1409        // Reset program dropdown with "Choose Program" after campus selection
1410        initializeDropdownWithDefault(programID, 'Choose Program');
1411
1412        if (campusValue) {
1413            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1414
1415            // Try to filter programs using mapping
1416            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1417            
1418            // If no mapping was applied, show all programs
1419            if (!mappingApplied) {
1420                // Filter programs based on selected campus
1421                    jQuery("#nf-field-" + programID + " option").each((index, option) => {
1422                    let optionCampuses = jQuery(option).attr('data-class');
1423                    if (typeof optionCampuses !== 'undefined' && campusValue !== '') {
1424                        optionCampuses = optionCampuses.split(' ');
1425                        if (optionCampuses.indexOf(campusValue) === -1 && option.value !== '') {
1426                            jQuery(option).remove();
1427                        }
1428                    }
1429                });
1430            }
1431
1432            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1433                if (jQuery(option).is(':empty')) {
1434                    jQuery(option).remove();
1435                }
1436            });
1437        }
1438
1439        // Reset dependent field (Semester) to "--"
1440        initializeDropdownWithDefault(semesterID, '--');
1441
1442        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1443        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1444
1445        // If only one Program remains available, auto-select it instead of
1446        // requiring the user to choose it manually.
1447        autoSelectSingleOption(programID);
1448
1449        runRuleEngine();
1450    });
1451
1452    // Program -> Semester filtering
1453    jQuery("#nf-field-" + programID).change(function () {
1454        const programValue = jQuery(this).val();
1455        const campusValue = jQuery("#nf-field-" + campusID).val();
1456
1457        // Reset semester dropdown with "Choose Semester" after program selection
1458        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1459
1460        if (programValue && campusValue) {
1461            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1462
1463            // Filter semesters based on selected program AND campus
1464            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1465                let optionSemesters = jQuery(option).attr('data-class');
1466                let optionPrograms = jQuery(option).attr('data-program-solo-id');
1467
1468                let shouldRemove = false;
1469
1470                // Filter by campus and program using data-class (format: "program-campus" pairs)
1471                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1472                    const semesterPairs = optionSemesters.split(' ');
1473                    let hasMatchingPair = false;
1474
1475                    semesterPairs.forEach(pair => {
1476                        const [programId, campusId] = pair.split('-');
1477                        if (programId === programValue && campusId === campusValue) {
1478                            hasMatchingPair = true;
1479                        }
1480                    });
1481
1482                    if (!hasMatchingPair && option.value !== '') {
1483                        shouldRemove = true;
1484                    }
1485                }
1486
1487                // Additional filter by program using data-program-solo-id
1488                if (!shouldRemove && typeof optionPrograms !== 'undefined' && programValue !== '') {
1489                    optionPrograms = optionPrograms.split(' ');
1490                    if (optionPrograms.indexOf(programValue) === -1 && option.value !== '') {
1491                        shouldRemove = true;
1492                    }
1493                }
1494
1495                if (shouldRemove) {
1496                    jQuery(option).remove();
1497                }
1498            });
1499
1500            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1501                if (jQuery(option).is(':empty')) {
1502                    jQuery(option).remove();
1503                }
1504            });
1505
1506            singleSemesterSelect(semesterID, campusID);
1507        }
1508
1509        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1510        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1511        runRuleEngine();
1512    });
1513
1514    // Semester change handler
1515    jQuery("#nf-field-" + semesterID).change(function () {
1516        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1517        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1518        runRuleEngine();
1519    });
1520
1521    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1522        runRuleEngine();
1523    })
1524    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1525        runRuleEngine();
1526    })
1527
1528    function runRuleEngine() {
1529
1530        const values = {
1531            program: jQuery("#nf-field-" + programID).val(),
1532            campus: jQuery("#nf-field-" + campusID).val(),
1533            semester: jQuery("#nf-field-" + semesterID).val(),
1534            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1535            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1536        };
1537
1538        const fieldIDs = {
1539            program: programID,
1540            campus: campusID,
1541            semester: semesterID,
1542            country: countryFieldId?.id,
1543            state: stateFieldId?.id
1544        };
1545
1546        // STEP 1: RESET everything first
1547        resetRuleEffects(fieldIDs);
1548
1549        // STEP 2: Apply rules fresh
1550        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1551    }
1552
1553    // If only one Campus is available on load, auto-select it. This cascades
1554    // (via the change handlers above) to auto-select Program and Semester too,
1555    // when each of those also only has a single available option.
1556    autoSelectSingleOption(campusID);
1557
1558    runRuleEngine(); // run once on load
1559}
1560
1561function programCampusSemesterChain(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1562    // Initialize dropdowns with defaults
1563    initializeDropdownWithDefault(campusID, 'Choose Campus');
1564    initializeDropdownWithDefault(semesterID, 'Choose Semester');
1565
1566    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
1567    programCampusDropDowns(programID, programLabelID, campusID, campusLabelID);
1568
1569    // Campus -> Semester filtering
1570    campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID);
1571}
1572
1573// CAMPUS > PROGRAM > SEMESTER chain (no school, useDynamic === 'campus')
1574function campusProgramSemesterChain(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1575    // Initialize dropdowns with defaults
1576    initializeDropdownWithDefault(programID, 'Choose Program');
1577    initializeDropdownWithDefault(semesterID, 'Choose Semester');
1578
1579    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1580    campusProgramDropDowns(programID, programLabelID, campusID, campusLabelID);
1581
1582    // Campus -> Semester filtering
1583    campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID);
1584}
1585
1586// UPDATED: Program -> Campus filtering with program_campus_mapping and fallback
1587function programCampusDropDowns(programID, programLabelID, campusID, campusLabelID) {
1588    // Remove any empty data-solo-campus-id options
1589    jQuery(`#nf-field-${programID} option[data-solo-campus-id=""]`).remove();
1590
1591    jQuery("#nf-field-" + programID).change(() => {
1592        const programValue = jQuery("#nf-field-" + programID).val();
1593
1594        // Reset campus dropdown with default option
1595        initializeDropdownWithDefault(campusID, 'Choose Campus');
1596
1597        if (programValue) {
1598            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1599
1600            // Try to filter using mapping, fallback to data-class if no mapping
1601            filterCampusesByProgramMapping(programValue, campusID, programID);
1602
1603            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1604                if (jQuery(option).is(':empty')) {
1605                    jQuery(option).remove();
1606                }
1607            });
1608        }
1609
1610        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1611        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1612    });
1613
1614    // Campus change handler for label updates
1615    jQuery("#nf-field-" + campusID).change(() => {
1616        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1617        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1618    });
1619}
1620
1621// UPDATED: Campus -> Program filtering with program_campus_mapping
1622function campusProgramDropDowns(programID, programLabelID, campusID, campusLabelID) {
1623    jQuery("#nf-field-" + campusID).change(() => {
1624        const campusValue = jQuery("#nf-field-" + campusID).val();
1625
1626        // Reset program dropdown with default option
1627        initializeDropdownWithDefault(programID, 'Choose Program');
1628
1629        if (campusValue) {
1630            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1631
1632            // Try to filter programs using mapping
1633            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1634            
1635            // If no mapping was applied, show all programs
1636            if (!mappingApplied) {
1637                jQuery("#nf-field-" + programID + " option").show();
1638                jQuery("#nf-field-" + programID + " option").prop('disabled', false);
1639            }
1640
1641            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1642                if (jQuery(option).is(':empty')) {
1643                    jQuery(option).remove();
1644                }
1645            });
1646        }
1647
1648        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1649        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1650    });
1651
1652    jQuery("#nf-field-" + programID).change(() => {
1653        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1654        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1655    });
1656}
1657
1658// UPDATED FUNCTION: Campus -> Semester filtering (standalone)
1659function campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID) {
1660    jQuery("#nf-field-" + campusID).change(function () {
1661        const campusValue = jQuery(this).val();
1662
1663        // Reset and populate semester dropdown with default
1664        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1665
1666        if (campusValue) {
1667            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1668
1669            // Filter semesters based on selected campus
1670            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1671                let optionSemesters = jQuery(option).attr('data-class');
1672
1673                if (typeof optionSemesters !== 'undefined' && campusValue !== '') {
1674                    optionSemesters = optionSemesters.split(' ');
1675                    if (optionSemesters.indexOf(campusValue) === -1 && option.value !== '') {
1676                        jQuery(option).remove();
1677                    }
1678                }
1679            });
1680
1681            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1682                if (jQuery(option).is(':empty')) {
1683                    jQuery(option).remove();
1684                }
1685            });
1686
1687            singleSemesterSelect(semesterID, campusID);
1688        }
1689
1690        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1691        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1692    });
1693
1694    // Semester change handler
1695    jQuery("#nf-field-" + semesterID).change(function () {
1696        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1697        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1698    });
1699}
1700
1701// Add the missing selector functions
1702function getUtmSchoolField(form) {
1703    return form.fields.find(f => f.key === 'utm_school');
1704}
1705
1706// Add the missing school initialization function for standalone school fields
1707function initializeSchoolField(schoolID, schoolLabelID) {
1708    jQuery("#nf-field-" + schoolID).change(function () {
1709        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1710        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1711    });
1712
1713    // Set initial value if there's a selected option
1714    const selectedSchool = jQuery("#nf-field-" + schoolID + " option:selected");
1715    if (selectedSchool.length > 0 && selectedSchool.val() !== '') {
1716        const initialSchoolLabel = selectedSchool.text();
1717        jQuery("#nf-field-" + schoolLabelID).val(initialSchoolLabel).trigger('change');
1718    }
1719}
1720
1721// Add the missing UTM function for schools
1722function selectSchoolUTM(schoolID, schoolLabelID, utmSchool) {
1723    if (utmSchool && utmSchool.value) {
1724        jQuery('#nf-field-' + schoolID).val(utmSchool.value).trigger('change');
1725        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1726        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1727    }
1728}
1729
1730// UPDATED FUNCTION: Directly load school options
1731function loadSchoolOptions(schoolID, schoolLabelID) {
1732
1733    const $schoolSelect = jQuery('#nf-field-' + schoolID);
1734    if (!$schoolSelect.length) {
1735        console.error('School select element not found: #nf-field-' + schoolID);
1736        return;
1737    }
1738
1739    // Clear existing options
1740    $schoolSelect.find('option').remove();
1741    $schoolSelect.find('optgroup').remove();
1742
1743    // Add default option first
1744    $schoolSelect.append('<option value="" label="">Choose School</option>');
1745
1746    // Append the schoolsHTML that's now available from PHP
1747    if (typeof ninjaFormDataJS.schoolsHTML !== 'undefined' && ninjaFormDataJS.schoolsHTML) {
1748        $schoolSelect.append(ninjaFormDataJS.schoolsHTML);
1749    } else {
1750        console.warn('ninjaFormDataJS.schoolsHTML is undefined');
1751    }
1752
1753    // Initialize the change handler
1754    initializeSchoolField(schoolID, schoolLabelID);
1755}
1756
1757// Add non-chained behavior for school forms
1758// Non-chained dropdown behavior for school forms (useDynamic === '')
1759function nonChainedSchoolDropDowns(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1760    // Initialize all dropdowns with their full options including defaults
1761    loadSchoolOptions(schoolID, schoolLabelID); // This now includes "Choose School"
1762
1763    // Initialize program field with all options
1764    const $programSelect = jQuery('#nf-field-' + programID);
1765    $programSelect.find('option').remove();
1766    $programSelect.find('optgroup').remove();
1767    $programSelect.append('<option value="" label="">Choose Program</option>');
1768    $programSelect.append(ninjaFormDataJS.programsHTML);
1769
1770    // Initialize campus field with all options  
1771    const $campusSelect = jQuery('#nf-field-' + campusID);
1772    $campusSelect.find('option').remove();
1773    $campusSelect.find('optgroup').remove();
1774    $campusSelect.append('<option value="" label="">Choose Campus</option>');
1775    $campusSelect.append(ninjaFormDataJS.campusesHTML);
1776
1777    // Initialize semester field with all options
1778    const $semesterSelect = jQuery('#nf-field-' + semesterID);
1779    $semesterSelect.find('option').remove();
1780    $semesterSelect.find('optgroup').remove();
1781    $semesterSelect.append('<option value="" label="">Choose Semester</option>');
1782    $semesterSelect.append(ninjaFormDataJS.semestersHTML);
1783
1784    // Set up change handlers for labels only (no filtering)
1785    jQuery("#nf-field-" + schoolID).change(function () {
1786        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1787        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1788        runRuleEngine();
1789    });
1790
1791    jQuery("#nf-field-" + programID).change(function () {
1792        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1793        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1794        runRuleEngine();
1795    });
1796
1797    jQuery("#nf-field-" + campusID).change(function () {
1798        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1799        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1800        runRuleEngine();
1801    });
1802
1803    jQuery("#nf-field-" + semesterID).change(function () {
1804        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1805        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1806        runRuleEngine();
1807    });
1808
1809    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1810        runRuleEngine();
1811    })
1812    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1813        runRuleEngine();
1814    })
1815
1816    function runRuleEngine() {
1817
1818        const values = {
1819            school: jQuery("#nf-field-" + schoolID).val(),
1820            program: jQuery("#nf-field-" + programID).val(),
1821            campus: jQuery("#nf-field-" + campusID).val(),
1822            semester: jQuery("#nf-field-" + semesterID).val(),
1823            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1824            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1825        };
1826
1827        const fieldIDs = {
1828            school: schoolID,
1829            program: programID,
1830            campus: campusID,
1831            semester: semesterID,
1832            country: countryFieldId?.id,
1833            state: stateFieldId?.id
1834        };
1835
1836        // STEP 1: RESET everything first
1837        resetRuleEffects(fieldIDs);
1838
1839        // STEP 2: Apply rules fresh
1840        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1841    }
1842
1843    // Since Program, Campus, and Semester are independent (non-chained) here,
1844    // auto-select each one on load if it only has a single available option.
1845    autoSelectSingleOption(programID);
1846    autoSelectSingleOption(campusID);
1847    autoSelectSingleOption(semesterID);
1848
1849    runRuleEngine(); // run once on load
1850}
1851
1852// Complete school > program > campus > semester chaining function
1853function schoolProgramCampusDropDowns(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1854    // Initialize school field
1855    initializeSchoolField(schoolID, schoolLabelID);
1856
1857    // When school changes, filter programs
1858    jQuery("#nf-field-" + schoolID).change(function () {
1859        const schoolValue = jQuery(this).val();
1860
1861        // Reset and populate program dropdown
1862        jQuery("#nf-field-" + programID + " option").remove();
1863        jQuery("#nf-field-" + programID + " optgroup").remove();
1864        jQuery("#nf-field-" + programID).append('<option value="" label="">Choose Program</option>');
1865        jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1866
1867        // Filter programs based on selected school
1868        jQuery("#nf-field-" + programID + " option").each((index, option) => {
1869            let optionSchools = jQuery(option).attr('data-schools');
1870            if (typeof optionSchools !== 'undefined' && schoolValue !== '') {
1871                optionSchools = optionSchools.split(' ');
1872                if (optionSchools.indexOf(schoolValue) === -1 && option.value !== '') {
1873                    jQuery(option).remove();
1874                }
1875            }
1876        });
1877
1878        jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1879            if (jQuery(option).is(':empty')) {
1880                jQuery(option).remove();
1881            }
1882        });
1883
1884        // Trigger program change to filter campuses
1885        jQuery("#nf-field-" + programID).trigger('change');
1886
1887        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1888        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1889    });
1890
1891    // Program change handler - UPDATED to use program_campus_mapping with fallback
1892    jQuery("#nf-field-" + programID).change(function () {
1893        const programValue = jQuery(this).val();
1894
1895        // Reset and populate campus dropdown
1896        jQuery("#nf-field-" + campusID + " option").remove();
1897        jQuery("#nf-field-" + campusID + " optgroup").remove();
1898        jQuery("#nf-field-" + campusID).append('<option value="" label="">Choose Campus</option>');
1899        jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1900
1901        if (programValue) {
1902            // Try to filter using mapping, fallback to data-class if no mapping
1903            filterCampusesByProgramMapping(programValue, campusID, programID);
1904        }
1905
1906        jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1907            if (jQuery(option).is(':empty')) {
1908                jQuery(option).remove();
1909            }
1910        });
1911
1912        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1913        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1914
1915        // Trigger campus change to filter semesters
1916        jQuery("#nf-field-" + campusID).trigger('change');
1917    });
1918
1919    // Campus change handler
1920    jQuery("#nf-field-" + campusID).change(function () {
1921        const campusValue = jQuery(this).val();
1922
1923        // Reset and populate semester dropdown
1924        jQuery("#nf-field-" + semesterID + " option").remove();
1925        jQuery("#nf-field-" + semesterID + " optgroup").remove();
1926        jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
1927        jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1928
1929        // Filter semesters based on selected campus
1930        jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1931            let optionSemesters = jQuery(option).attr('data-class');
1932
1933            if (typeof optionSemesters !== 'undefined' && campusValue !== '') {
1934                optionSemesters = optionSemesters.split(' ');
1935                if (optionSemesters.indexOf(campusValue) === -1 && option.value !== '') {
1936                    jQuery(option).remove();
1937                }
1938            }
1939        });
1940
1941        jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1942            if (jQuery(option).is(':empty')) {
1943                jQuery(option).remove();
1944            }
1945        });
1946
1947        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1948        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1949
1950        singleSemesterSelect(semesterID, campusID);
1951    });
1952
1953    // Semester change handler
1954    jQuery("#nf-field-" + semesterID).change(function () {
1955        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1956        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1957    });
1958}
1959
1960//selector functions
1961
1962function getPageUrlField(form) {
1963    return form.fields.find(f => f.key === 'page_url');
1964}
1965
1966function getThankYouPageField(form) {
1967    return form.fields.find(f => f.key === 'thank_you_page');
1968}
1969
1970function getSubmitButton(form) {
1971    return form.fields.find(f => f.key.includes('submit'));
1972}
1973
1974function getTimezoneField(form) {
1975    return form.fields.find(f => f.key === 'selected_timezone');
1976}
1977
1978function getProgramSelectField(form) {
1979    return form.fields.find(f => f.key === 'program_select');
1980}
1981
1982function getCampusSelectField(form) {
1983    return form.fields.find(f => f.key === 'campus_select');
1984}
1985
1986function getCampusDataField(form) {
1987    return form.fields.find(f => f.key === 'use_campus_data_attribute');
1988}
1989
1990function getUtmCampusField(form) {
1991    return form.fields.find(f => f.key === 'utm_campus');
1992}
1993
1994// School Field Functions
1995function getSchoolSelectField(form) {
1996    return form.fields.find(f => f.key === 'school_select');
1997}
1998
1999function getSchoolLabelField(form) {
2000    return form.fields.find(f => f.key === 'school_label');
2001}
2002
2003function getCampusLabelField(form) {
2004    return form.fields.find(f => f.key === 'campus_label');
2005}
2006
2007function getUtmProgramField(form) {
2008    return form.fields.find(f => f.key === 'utm_program');
2009}
2010
2011function getProgramLabelField(form) {
2012    return form.fields.find(f => f.key === 'program_label');
2013}
2014
2015function getSemesterSelectField(form) {
2016    return form.fields.find(f => f.key === 'semester_select');
2017}
2018
2019function getSemesterLabelField(form) {
2020    return form.fields.find(f => f.key === 'semester_label');
2021}
2022
2023function getSrcParamTranslatedField(form) {
2024    return form.fields.find(f => f.key === 'src_parameter_translated');
2025}
2026
2027function getCountryZipTypeField(form) {
2028    return form.fields.find(f => f.key === 'country_zip_type');
2029}
2030
2031function getZipStatusField(form) {
2032    return form.fields.find(f => f.key === 'zip_status');
2033}
2034
2035function getPostalCodeField(form) {
2036    return form.fields.find(f => f.key === 'postal_code');
2037}
2038
2039function getZipCodeField(form) {
2040    return form.fields.find(f => f.key === 'zip_code');
2041}
2042
2043function getCountryField(form) {
2044    return form.fields.find(f => f.key === 'list_country_main');
2045}
2046
2047function getCityField(form) {
2048    return form.fields.find(f => f.key === 'visible_city');
2049}
2050
2051function getStateField(form) {
2052    return form.fields.find(f => f.key === 'visible_state');
2053}
2054
2055function getProvinceField(form) {
2056    return form.fields.find(f => f.key === 'province_ca');
2057}
2058
2059function getInternationalStateField(form) {
2060    return form.fields.find(f => f.key === 'int_state');
2061}
2062
2063function getLocalLatField(form) {
2064    return form.fields.find(f => f.key === 'local_latitude');
2065}
2066
2067function getLocalLongField(form) {
2068    return form.fields.find(f => f.key === 'local_longitude');
2069}
2070
2071function getCountryCodeField(form) {
2072    return form.fields.find(f =>
2072 f.key === 'gdpr_country_code');
2073}
2074
2075function getEmailField(form) {
2076    return form.fields.find(f => f.type === 'email');
2077}
2078
2079function getDateLimit(form) {
2080    const i = ninjaFormDataJS.ninjaFormsIDs.indexOf(form.id);
2081    if (ninjaFormDataJS.ninjaDateSettings[i] > 0) return Number(ninjaFormDataJS.ninjaDateSettings[i]);
2082    else return 0;
2083}
2084
2085// Looks up this form's per-form "Data Sync" setting (Campus First / Program
2086// First / None), configured under DataFlow Sync > General > Campus-Program
2087// Chaining and localized as ninjaFormDataJS.formCampusSettings, keyed to
2088// ninjaFormDataJS.ninjaFormsIDs by form ID.
2089function getFormDataSyncSetting(form) {
2090    const i = ninjaFormDataJS.ninjaFormsIDs.indexOf(form.id);
2091    return i > -1 ? ninjaFormDataJS.formCampusSettings[i] : undefined;
2092}
2093
2094function useDynamicCampusProgram(form) {
2095    const dataSyncSetting = getFormDataSyncSetting(form); // 'campus' | 'program' | 'no' | undefined
2096
2097    // Page-level override: the "Form is Program First?" checkbox in the
2098    // Page Editor (TCS Field Config metabox) FORCIBLY wins over the Data
2099    // Sync setting whenever it's checked -- e.g. Data Sync says
2100    // "Campus First" (or even "None") but this page has the checkbox
2101    // checked -> use Program First regardless.
2102    const pageConfig = ninjaFormDataJS.pageFieldConfig;
2103    if (pageConfig && pageConfig.program_first) {
2104        return 'program';
2105    }
2106
2107    // Data Sync explicitly says this form uses no campus/program chaining at
2108    // all, and the checkbox above didn't override it -- no chaining.
2109    if (dataSyncSetting === 'no') {
2110        return false;
2111    }
2112
2113    // No page-level override -- default to whatever Data Sync says for this form.
2114    if (dataSyncSetting === 'campus') return 'campus';
2115    if (dataSyncSetting === 'program') return 'program';
2116
2117    // No Data Sync entry configured for this form -- fall back to the
2118    // Program-first progressive-filtering chain -- Program shows every
2119    // option, Campus and Semester stay empty until a Program is chosen.
2120    return 'program';
2121}
2122
2123//custom NinjaForm features
2124function captureURL(pageUrlId) {
2125    jQuery('#nf-field-' + pageUrlId).val(window.location.href).trigger('change');
2126}
2127
2128function setThankYouPage(thankYouPageId) {
2129    if (ninjaFormDataJS.thankYouPage !== 'no') {
2130        jQuery('#nf-field-' + thankYouPageId).val(ninjaFormDataJS.thankYouPage).trigger('change');
2131    }
2132}
2133
2134function getLatLong(localLatitudeId, localLongitudeId, submitButtonId) {
2135    jQuery(`#nf-field-${submitButtonId}`).prop('disabled', true);
2136    jQuery.ajax({
2137        url: `https://www.googleapis.com/geolocation/v1/geolocate?key=${ninjaFormDataJS.googleAPIKey}`,
2138        cache: false,
2139        dataType: "json",
2140        type: "POST",
2141        complete: () => {
2142            jQuery(`#nf-field-${submitButtonId}`).prop('disabled', false);
2143        },
2144        success: result => {
2145            const lat = result.location.lat;
2146            const lng = result.location.lng;
2147            jQuery(`#nf-field-${localLatitudeId}`).val(lat).trigger('change');
2148            jQuery(`#nf-field-${localLongitudeId}`).val(lng).trigger('change');
2149        },
2150        error: err => { console.log(err); }
2151    });
2152}
2153
2154function gdprRequirements(countryCodeID) {
2155    jQuery.ajax({
2156        url: `https://www.googleapis.com/geolocation/v1/geolocate?key=${ninjaFormDataJS.googleAPIKey}`,
2157        cache: false,
2158        dataType: 'json',
2159        type: 'POST',
2160        success: result => {
2161            const lat = result.location.lat;
2162            const lng = result.location.lng;
2163            jQuery.ajax({
2164                url: `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&language=en&key=${ninjaFormDataJS.googleAPIKey}`,
2165                cache: false,
2166                dataType: 'json',
2167                type: 'GET',
2168                success: result => {
2169                    const countryCode = result.results.find(x => x.types.indexOf('country') >= 0).address_components[0].short_name.toUpperCase();
2170                    jQuery(`#nf-field-${countryCodeID}`).val(countryCode).trigger('change');
2171                },
2172                error: err => { console.log(err); }
2173            });
2174        },
2175        error: err => { console.log(err); }
2176    });
2177}
2178
2179function validateZipCode(countryFieldID, countryZipTypeID, zipFieldID, postalFieldID, stateFieldID, provFieldID, intStateFieldID, countryZipStatusID, cityFieldID, submitButtonID) {
2180    const zipCodeTimeZoneValidationController = Marionette.Object.extend({
2181        initialize: function () {
2182            const fieldsChannel = Backbone.Radio.channel('fields');
2183            this.listenTo(fieldsChannel, 'change:modelValue', this.validate);
2184        },
2185        validate: model => {
2186            if (model.get('type') !== 'zip' && model.get('type') !== 'listcountry') return;
2187            const fieldValue = model.get('value');
2188            const countriesNoPostal = [{
2189                "name": "Angola",
2190                "code": "AO"
2191            }, {
2192                "name": "Antigua and Barbuda",
2193                "code": "AG"
2194            }, {
2195                "name": "Aruba",
2196                "code": "AW"
2197            }, {
2198                "name": "Bahamas",
2199                "code": "BS"
2200            }, {
2201                "name": "Belize",
2202                "code": "BZ"
2203            }, {
2204                "name": "Benin",
2205                "code": "BJ"
2206            }, {
2207                "name": "Botswana",
2208                "code": "BW"
2209            }, {
2210                "name": "Burkina Faso",
2211                "code": "BF"
2212            }, {
2213                "name": "Burundi",
2214                "code": "BI"
2215            }, {
2216                "name": "Cameroon",
2217                "code": "CM"
2218            }, {
2219                "name": "Central African Republic",
2220                "code": "CF"
2221            }, {
2222                "name": "Comoros",
2223                "code": "KM"
2224            }, {
2225                "name": "Congo",
2226                "code": "CG"
2227            }, {
2228                "name": "Congo, the Democratic Republic of the",
2229                "code": "CD"
2230            }, {
2231                "name": "Cook Islands",
2232                "code": "CK"
2233            }, {
2234                "name": "Cote d'Ivoire",
2235                "code": "CI"
2236            }, {
2237                "name": "Djibouti",
2238                "code": "DJ"
2239            }, {
2240                "name": "Dominica",
2241                "code": "DM"
2242            }, {
2243                "name": "Equatorial Guinea",
2244                "code": "GQ"
2245            }, {
2246                "name": "Eritrea",
2247                "code": "ER"
2248            }, {
2249                "name": "Fiji",
2250                "code": "FJ"
2251            }, {
2252                "name": "French Southern Territories",
2253                "code": "TF"
2254            }, {
2255                "name": "Gambia",
2256                "code": "GM"
2257            }, {
2258                "name": "Ghana",
2259                "code": "GH"
2260            }, {
2261                "name": "Grenada",
2262                "code": "GD"
2263            }, {
2264                "name": "Guinea",
2265                "code": "GN"
2266            }, {
2267                "name": "Guyana",
2268                "code": "GY"
2269            }, {
2270                "name": "Hong Kong",
2271                "code": "HK"
2272            }, {
2273                "name": "Ireland",
2274                "code": "IE"
2275            }, {
2276                "name": "Israel",
2277                "code": "IL"
2278            }, {
2279                "name": "Jamaica",
2280                "code": "JM"
2281            }, {
2282                "name": "Kenya",
2283                "code": "KE"
2284            }, {
2285                "name": "Kiribati",
2286                "code": "KI"
2287            }, {
2288                "name": "Macao",
2289                "code": "MO"
2290            }, {
2291                "name": "Malawi",
2292                "code": "MW"
2293            }, {
2294                "name": "Mali",
2295                "code": "ML"
2296            }, {
2297                "name": "Mauritania",
2298                "code": "MR"
2299            }, {
2300                "name": "Mauritius",
2301                "code": "MU"
2302            }, {
2303                "name": "Montserrat",
2304                "code": "MS"
2305            }, {
2306                "name": "Nauru",
2307                "code": "NR"
2308            }, {
2309                "name": "Netherlands Antilles",
2310                "code": "AN"
2311            }, {
2312                "name": "Nigeria",
2313                "code": "NG"
2314            }, {
2315                "name": "Niue",
2316                "code": "NU"
2317            }, {
2318                "name": "North Korea",
2319                "code": "KP"
2320            }, {
2321                "name": "Panama",
2322                "code": "PA"
2323            }, {
2324                "name": "Qatar",
2325                "code": "QA"
2326            }, {
2327                "name": "Rwanda",
2328                "code": "RW"
2329            }, {
2330                "name": "Saint Kitts and Nevis",
2331                "code": "KN"
2332            }, {
2333                "name": "Saint Lucia",
2334                "code": "LC"
2335            }, {
2336                "name": "Sao Tome and Principe",
2337                "code": "ST"
2338            }, {
2339                "name": "Saudi Arabia",
2340                "code": "SA"
2341            }, {
2342                "name": "Seychelles",
2343                "code": "SC"
2344            }, {
2345                "name": "Sierra Leone",
2346                "code": "SL"
2347            }, {
2348                "name": "Solomon Islands",
2349                "code": "SB"
2350            }, {
2351                "name": "Somalia",
2352                "code": "SO"
2353            }, {
2354                "name": "South Africa",
2355                "code": "ZA"
2356            }, {
2357                "name": "Suriname",
2358                "code": "SR"
2359            }, {
2360                "name": "Syria",
2361                "code": "SY"
2362            }, {
2363                "name": "Tanzania, United Republic of",
2364                "code": "TZ"
2365            }, {
2366                "name": "Timor-Leste",
2367                "code": "TL"
2368            }, {
2369                "name": "Tokelau",
2370                "code": "TK"
2371            }, {
2372                "name": "Tonga",
2373                "code": "TO"
2374            }, {
2375                "name": "Trinidad and Tobago",
2376                "code": "TT"
2377            }, {
2378                "name": "Tuvalu",
2379                "code": "TV"
2380            }, {
2381                "name": "Uganda",
2382                "code": "UG"
2383            }, {
2384                "name": "United Arab Emirates",
2385                "code": "AE"
2386            }, {
2387                "name": "Vanuatu",
2388                "code": "VU"
2389            }, {
2390                "name": "Yemen",
2391                "code": "YE"
2392            }, {
2393                "name": "Zimbabwe",
2394                "code": "ZW"
2395            }];
2396            if (model.get('type') === 'listcountry') {
2397                jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2398                jQuery('#nf-field-' + postalFieldID).val("");
2399                jQuery('#nf-field-' + zipFieldID).val("");
2400                jQuery('#nf-field-' + intStateFieldID).val("").trigger('change');
2401                jQuery('#nf-field-' + cityFieldID).val("");
2402                Backbone.Radio.channel('fields').request('remove:error', postalFieldID, 'required-error');
2403                if (fieldValue === "US") {
2404                    jQuery('#nf-field-' + countryZipTypeID).val("us").trigger('change');
2405                } else if (fieldValue === "CA") {
2406                    jQuery('#nf-field-' + countryZipTypeID).val("ca").trigger('change');
2407                } else {
2408                    const countryPostalResult = jQuery.map(countriesNoPostal, val => {
2409                        return val.code === fieldValue ? 'nopost' : null;
2410                    });
2411                    if (countryPostalResult[0] === 'nopost') {
2412                        jQuery('#nf-field-' + countryZipTypeID).val("nopost").trigger('change');
2413                        jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2414                        jQuery("label[for=nf-field-" + postalFieldID + "]").html('Postal Code');
2415                        jQuery('#nf-field-' + cityFieldID).val("");
2416                        jQuery('#nf-field-' + intStateFieldID).val("").trigger('change');
2417                        Backbone.Radio.channel('fields').request('remove:error', postalFieldID, 'required-error');
2418                        Backbone.Radio.channel('fields').request('remove:error', zipFieldID, 'required-error');
2419                    } else {
2420                        jQuery('#nf-field-' + countryZipTypeID).val("int").trigger('change');
2421                        jQuery("label[for=nf-field-" + postalFieldID + "]").html('Postal Code <span class="ninja-forms-req-symbol">*</span>');
2422                    }
2423                }
2424            }
2425            if (model.get('type') === 'zip') {
2426                if (!model.get('value')) return;
2427                console.log('ZIP VALIDATION START - zipValue:', model.get('value'), 'countryZipType:', jQuery('#nf-field-' + countryZipTypeID).val());
2428                if (jQuery('#nf-field-' + countryZipTypeID).val() === 'nopost') {
2429                    jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2430                } else {
2431                    jQuery('#nf-field-' + zipFieldID).parent().append('<img src="' + ninjaFormDataJS.loadingGifPath + '" alt="tcspp-loader" class="zip-loader" width="118" height="15" />');
2432                    jQuery('#nf-field-' + zipFieldID).prop('disabled', true);
2433                    jQuery('#nf-field-' + submitButtonID).prop('disabled', true);
2434                    const country = jQuery(`#nf-field-${countryFieldID}`).find(':selected').text();
2435                    const apiKey = ninjaFormDataJS.googleAPIKey || 'AIzaSyBadUrU21eiyl9PJ0I7VovULzAPwlL7FUo';
2436                    const searchAddress = model.get('value') + ' ' + country;
2437                    const apiUrl = "https://maps.googleapis.com/ma
2437ps/api/geocode/json?address=" + encodeURIComponent(searchAddress) + '&language=en' + "&key=" + apiKey;
2438                    
2439                    console.log('ZIP API CALL:', {
2440                        zipValue: model.get('value'),
2441                        country: country,
2442                        searchAddress: searchAddress,
2443                        hasAPIKey: !!apiKey,
2444                        apiKeyLength: apiKey ? apiKey.length : 0,
2445                        url: apiUrl.substring(0, 100) + '...'
2446                    });
2447                    
2448                    jQuery.ajax({
2449                        url: apiUrl,
2450                        cache: false,
2451                        dataType: "json",
2452                        type: "GET",
2453                        complete: () => {
2454                            console.log('ZIP AJAX COMPLETE');
2455                            jQuery('.zip-loader').remove();
2456                            jQuery('#nf-field-' + zipFieldID).prop('disabled', false);
2457                            jQuery('#nf-field-' + submitButtonID).prop('disabled', false);
2458                        },
2459                        success: result => {
2460                            console.log('ZIP AJAX SUCCESS - results length:', result.results.length);
2461                            console.log('ZIP AJAX FULL RESPONSE:', result);
2462                            
2463                            // Handle API key restriction errors - but validate format first
2464                            if (result.status === 'REQUEST_DENIED') {
2465                                console.log('ZIP - API Request Denied (likely referer restriction). Validating format...');
2466                                
2467                                // Basic format validation for common zip codes
2468                                const zipValue = model.get('value');
2469                                const countryZipType = jQuery('#nf-field-' + countryZipTypeID).val();
2470                                let isValidFormat = false;
2471                                
2472                                if (countryZipType === 'us') {
2473                                    // US: 5 digits (12345) or 5+4 (12345-6789)
2474                                    isValidFormat = /^\d{5}(-\d{4})?$/.test(zipValue);
2475                                } else if (countryZipType === 'ca') {
2476                                    // Canada: A1A 1A1 format (letter-digit-letter space digit-letter-digit)
2477                                    isValidFormat = /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i.test(zipValue);
2478                                } else if (countryZipType === 'int') {
2479                                    // International: at least 3 characters, alphanumeric
2480                                    isValidFormat = /^[A-Z0-9\s\-]{3,}$/i.test(zipValue);
2481                                }
2482                                
2483                                if (isValidFormat) {
2484                                    console.log('ZIP - Format valid. Accepting zip code despite API restriction.');
2485                                    Backbone.Radio.channel('fields').request('remove:error', model.get('id'), 'zip-code-error');
2486                                    jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2487                                } else {
2488                                    console.log('ZIP - Format invalid for type:', countryZipType, 'value:', zipValue);
2489                                    Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2490                                    jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2491                                }
2492                                return;
2493                            }
2494                            
2495                            if (typeof jQuery('#nf-field-' + countryFieldID).val() === 'undefined') return;
2496                            if (result.results.length > 0) {
2497                                const address = result.results.find(r => typeof r.address_components.find(ac => typeof ac.types.find(t => t === 'postal_code') !== 'undefined') !== 'undefined');
2498                                
2499                                console.log('ZIP - Found address with postal code:', typeof address !== 'undefined');
2500                                if (typeof address !== 'undefined') {
2501                                    console.log('ZIP - Address components:', address.address_components.map(c => ({types: c.types, short_name: c.short_name})));
2502                                    
2503                                    if (address.address_components.find(ac => typeof ac.types.find(t => t === 'country') !== 'undefined').short_name === jQuery('#nf-field-' + countryFieldID).val()) {
2504                                        Backbone.Radio.channel('fields').request('remove:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2505                                        jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2506                                        if (address.address_components.find(ac => typeof ac.types.find(t => t === 'country') !== 'undefined').short_name === 'CA' || address.address_components.find(ac => typeof ac.types.find(t => t === 'country') !== 'undefined').short_name === 'US') {
2507                                            // Find components by type, not by index
2508                                            const localityComponent = address.address_components.find(ac => ac.types.includes('locality'));
2509                                            const stateComponent = address.address_components.find(ac => ac.types.includes('administrative_area_level_1'));
2510                                            
2511                                            console.log('ZIP - Found city:', localityComponent?.short_name, 'state:', stateComponent?.short_name);
2512                                            
2513                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'us') {
2514                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2515                                                if (stateComponent) jQuery('#nf-field-' + stateFieldID).val(stateComponent.short_name).trigger('change');
2516                                            }
2517                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'ca') {
2518                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2519                                                if (stateComponent) jQuery('#nf-field-' + provFieldID).val(stateComponent.short_name).trigger('change');
2520                                            }
2521                                        } else {
2522                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'int') {
2523                                                const localityComponent = address.address_components.find(ac => ac.types.includes('locality'));
2524                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2525                                            }
2526                                        }
2527                                    } else {
2528                                        console.log('ZIP - Country mismatch');
2529                                        Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid ' + (jQuery('#nf-field-' + countryFieldID + " > option[value='" + (jQuery('#nf-field-' + countryFieldID).val()) + "']").text()) + ' postal code.');
2530                                        jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2531                                    }
2532                                }
2533                                else {
2534                                    console.log('ZIP - No address with postal code found');
2535                                    Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2536                                }
2537                            }
2538                            else {
2539                                console.log('ZIP - No results returned');
2540                                Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2541                            }
2542                        },
2543                        error: (xhr, status, error) => {
2544                            console.log('ZIP AJAX ERROR:', {status, error, responseText: xhr.responseText});
2545                            // Only accept on REQUEST_DENIED (API restriction), reject on other errors
2546                            Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2547                            jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2548                        }
2549                    });
2550                }
2551            }
2552        }
2553    });
2554    new zipCodeTimeZoneValidationController();
2555}
2556
2557function setEmailValidation() {
2558    const emailValidator = Marionette.Object.extend({
2559        initialize: function () {
2560            const submitChannel = Backbone.Radio.channel('submit');
2561            this.listenTo(submitChannel, 'validate:field', this.validate);
2562            const fieldsChannel = Backbone.Radio.channel('fields');
2563            this.listenTo(fieldsChannel, 'change:modelValue', this.validate);
2564        },
2565        validate: function (model) {
2566            if (model.get('type') !== 'email') return;
2567            const fieldValue = model.get('value');
2568            const modelId = model.get('id');
2569            const errorId = 'custom-email-error';
2570            const errorMessage = 'Please enter a valid email address!';
2571            const fieldsChannel = Backbone.Radio.channel('fields');
2572            if (/^[A-z|0-9|.]+@[A-z|0-9|.]+.[A-z]+$/.test(fieldValue)) {
2573                fieldsChannel.request('remove:error', modelId, errorId);
2574            } else {
2575                fieldsChannel.request('add:error', modelId, errorId, errorMessage);
2576            }
2577        }
2578    });
2579    new emailValidator();
2580}
2581
2582function limitDates(dateLimit) {
2583    const dateLimiter = Marionette.Object.extend({
2584        initialize: function () {
2585            const pikadayChannel = Backbone.Radio.channel('pikaday');
2586            this.listenTo(pikadayChannel, 'init', this.modifyDatepicker);
2587        },
2588        modifyDatepicker: (dateObject, fieldModel) => {
2589            let currentDate = new Date();
2590            const tomorrow = currentDate.setDate(currentDate.getDate() + 1);
2591            currentDate = new Date();
2592            const futureweek = currentDate.setDate(currentDate.getDate() + dateLimit);
2593            dateObject.pikaday._o.disableWeekends = true;
2594            dateObject.pikaday._o.firstDay = 0;
2595            dateObject.pikaday._o.maxDate = futureweek;
2596            dateObject.pikaday._o.minDate = tomorrow;
2597            dateObject.pikaday._o.disableDayFn = (date) => {
2598                const disableDays = ninjaFormDataJS.holidayBlackOuts;
2599                if (_.indexOf(disableDays, moment(date).format("YYYY-MM-DD")) !== -1) {
2600                    return true;
2601                }
2602            }
2603        }
2604    });
2605    new dateLimiter();
2606}
2607
2608function getLeadSourceID(srcParamTranslatedID) {
2609    const leadSourceID = ninjaFormDataJS.leadSourceID;
2610
2611    jQuery('#nf-field-' + srcParamTranslatedID)
2612        .val(leadSourceID)
2613        .trigger('change');
2614
2615    // Safely push to dataLayer only if it exists
2616    if (typeof window.dataLayer !== 'undefined' && Array.isArray(window.dataLayer)) {
2617        window.dataLayer.push({
2618            'tcsLeadSourceID': leadSourceID
2619        });
2620    } else {
2621        console.warn('dataLayer not found: skipping GTM push');
2622    }
2623}
2624
2625function programCampusDropDowns(programID, programLabelID, campusID, campusLabelID) {
2626    // Remove any empty data-solo-campus-id options
2627    jQuery(`#nf-field-${programID} option[data-solo-campus-id=""]`).remove();
2628
2629    jQuery("#nf-field-" + programID).change(() => {
2630        const programValue = jQuery("#nf-field-" + programID).val();
2631
2632        // Reset campus dropdown with default option
2633        initializeDropdownWithDefault(campusID, 'Choose Campus');
2634
2635        if (programValue) {
2636            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
2637
2638            // Try to filter using mapping, fallback to data-class if no mapping
2639            filterCampusesByProgramMapping(programValue, campusID, programID);
2640
2641            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
2642                if (jQuery(option).is(':empty')) {
2643                    jQuery(option).remove();
2644                }
2645            });
2646        }
2647
2648        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2649        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2650    });
2651
2652    // Campus change handler for label updates
2653    jQuery("#nf-field-" + campusID).change(() => {
2654        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2655        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2656    });
2657}
2658
2659// Start for semester based on campus
2660function programSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID, programID, programLabelID, selectedFirst) {
2661
2662    jQuery("#nf-field-" + semesterID + " option").remove();
2663    jQuery("#nf-field-" + semesterID + " optgroup").remove();
2664    jQuery("#nf-field-" + semesterID).append('<option value="" label="">--</option>');
2665
2666    let selectedProgramSemesterHTML = '';
2667    let selectedCampusSemesterHTML = '';
2668    let semester_selected_ProgValue = '';
2669
2670    if (selectedFirst == 'program') {
2671        jQuery("#nf-field-" + programID).change(() => {
2672            const semesterProgValue = jQuery("#nf-field-" + programID).val();
2673            semester_selected_ProgValue = semesterProgValue;
2674            jQuery("#nf-field-" + semesterID + " option").remove();
2675            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2676            jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
2677            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
2678
2679            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2680                let optionSemestersProgid = jQuery(option).attr('data-program-solo-id');
2681
2682                if (typeof optionSemestersProgid !== 'undefined' && semesterProgValue !== '') {
2683                    optionSemestersProgid = optionSemestersProgid.split(' ');
2684                    var Semester_len = optionSemestersProgid.length;
2685
2686                    if (optionSemestersProgid.indexOf(semesterProgValue) === 0) {
2687                        //option.setAttribute('selected','selected');
2688                    }
2689                    if (optionSemestersProgid.indexOf(semesterProgValue) === -1 && option.value !== '') {
2690                        jQuery(option).remove();
2691                    }
2692                }
2693            });
2694
2695            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2696                if (jQuery(option).is(':empty')) {
2697                    jQuery(option).remove();
2698                }
2699            });
2700
2701            selectedProgramSemesterHTML = jQuery("#nf-field-" + semesterID).html();
2702
2703        });
2704
2705        jQuery("#nf-field-" + campusID).change(() => {
2706            const semesterValue = jQuery("#nf-field-" + campusID).val();
2707            jQuery("#nf-field-" + semesterID + " option").remove();
2708            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2709            jQuery("#nf-field-" + semesterID).append(selectedProgramSemesterHTML);
2710
2711            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2712                let option_value_Semesters = jQuery(option).attr('data-class');
2713                let optionSemesters = '';
2714
2715                if (typeof option_value_Semesters !== 'undefined' && semesterValue !== '') {
2716                    optionSemesters = option_value_Semesters
2717                        .split(" ") // Split the string into an array
2718                        .filter(pair => pair.startsWith(semester_selected_ProgValue + "-")) // Filter pairs matching the selected ID
2719                        .map(pair => pair.split("-")[1]) // Extract only the values
2720                        .join(" ");
2721                }
2722
2723                if (typeof optionSemesters !== 'undefined' && semesterValue !== '') {
2724                    optionSemesters = optionSemesters.split(' ');
2725                    var Semester_len = optionSemesters.length;
2726
2727                    if (optionSemesters.indexOf(semesterValue) === 0) {
2728                        //option.setAttribute('selected','selected');
2729                    }
2730                    if (optionSemesters.indexOf(semesterValue) === -1 && option.value !== '') {
2731                        jQuery(option).remove();
2732                    }
2733                }
2734            });
2735
2736            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2737                if (jQuery(option).is(':empty')) {
2738                    jQuery(option).remove();
2739                }
2740            });
2741
2742            const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2743            jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2744
2745            singleSemesterSelect(semesterID, campusID);
2746        });
2747        jQuery("#nf-field-" + semesterID).change(() => {
2748            const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2749            jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2750        });
2751
2752    } else if (selectedFirst == 'campus') {
2753
2754        jQuery("#nf-field-" + campusID).change(() => {
2755            const semesterValue = jQuery("#nf-field-" + campusID).val();
2756            jQuery("#nf-field-" + semesterID + " option").remove();
2757            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2758            jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
2759            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
2760
2761            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2762                let optionSemesters = jQuery(option).attr('data-class');
2763
2764                if (typeof optionSemesters !== 'undefined' && semesterValue !== '') {
2765                    optionSemesters = optionSemesters.split(' ');
2766                    var Semester_len = optionSemesters.length;
2767
2768                    if (optionSemesters.indexOf(semesterValue) === 0) {
2769                        //option.setAttribute('selected','selected');
2770                    }
2771                    if (optionSemesters.indexOf(semesterValue) === -1 && option.value !== '') {
2772                        jQuery(option).remove();
2773                    }
2774                }
2775            });
2776
2777            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2778                if (jQuery(option).is(':empty')) {
2779                    jQuery(option).remove();
2780                }
2781            });
2782
2783            selectedCampusSemesterHTML = jQuery("#nf-field-" + semesterID).html();
2784
2785            const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2786            jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2787
2788            singleSemesterSelect(semesterID, campusID);
2789        });
2790
2791        jQuery("#nf-field-" + programID).change(() => {
2792            const semesterProgValue = jQuery("#nf-field-" + programID).val();
2793            jQuery("#nf-field-" + semesterID + " option").remove();
2794            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2795            jQuery("#nf-field-" + semesterID).append(selectedCampusSemesterHTML);
2796
2797            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2798                let optionSemestersProgid = jQuery(option).attr('data-program-solo-id');
2799
2800                if (typeof optionSemestersProgid !== 'undefined' && semesterProgValue !== '') {
2801                    optionSemestersProgid = optionSemestersProgid.split(' ');
2802                    var Semester_len = optionSemestersProgid.length;
2803
2804                    if (optionSemestersProgid.indexOf(semesterProgValue) === 0) {
2805                        //option.setAttribute('selected','selected');
2806                    }
2807                    if (optionSemestersProgid.indexOf(semesterProgValue) === -1 && option.value !== '') {
2808                        jQuery(option).remove();
2809                    }
2810                }
2811            });
2812
2813            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2814                if (jQuery(option).is(':empty')) {
2815                    jQuery(option).remove();
2816                }
2817            });
2818        });
2819
2820        jQuery("#nf-field-" + semesterID).change(() => {
2821            const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2822            jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2823        });
2824
2825    }
2826
2827}
2828
2829function singleSemesterSelect(semesterID, campusID) {
2830    var $select = jQuery("#nf-field-" + semesterID);
2831    var optionsWithValue = $select.find("option").filter(function () {
2832        return jQuery(this).val().trim() !== "";
2833    });
2834
2835    var optionCountWithValue = optionsWithValue.length;
2836
2837    if (optionCountWithValue === 1) {
2838        optionsWithValue.prop("selected", true);
2839        jQuery("#nf-field-" + semesterID).trigger('change');
2840    }
2841}
2842
2843// End for semester
2844
2845function nonChainedDropDowns(programID, programLabelID, campusID, campusLabelID, campusDataField) {
2846    // Initialize both dropdowns with default options
2847    initializeDropdownWithDefault(programID, '--');
2848    initializeDropdownWithDefault(campusID, '--');
2849
2850    jQuery("#nf-field-" + campusID).change(function () {
2851        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2852        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2853    });
2854
2855    jQuery("#nf-field-" + programID).change(function () {
2856        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2857        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2858
2859        if (typeof campusDataField !== 'undefined') {
2860            const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2861            jQuery("#nf-field-" + campusDataField.id).val(dataSoloCampusID).trigger('change');
2862            if (typeof dataSoloCampusID !== 'undefined') {
2863                const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2864                    return ninjaFormDataJS.campusData[key];
2865                });
2866                const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2867                if (typeof soloCampus !== 'undefined') {
2868                    jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2869                }
2870            }
2871        }
2872    });
2873}
2874
2875// Non-chained dropdown behavior for forms without school (useDynamic === '')
2876function nonChainedDropDownsNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, campusDataField, countryFieldId, stateFieldId) {
2877
2878    // Initialize all dropdowns with their full options including defaults
2879    // Initialize program field with all options
2880    const $programSelect = jQuery('#nf-field-' + programID);
2881    $programSelect.find('option').remove();
2882    $programSelect.find('optgroup').remove();
2883    $programSelect.append('<option value="" label="">Choose Program</option>');
2884    $programSelect.append(ninjaFormDataJS.programsHTML);
2885
2886    // Initialize campus field with all options  
2887    const $campusSelect = jQuery('#nf-field-' + campusID);
2888    $campusSelect.find('option').remove();
2889    $campusSelect.find('optgroup').remove();
2890    $campusSelect.append('<option value="" label="">Choose Campus</option>');
2891    $campusSelect.append(ninjaFormDataJS.campusesHTML);
2892
2893    // Initialize semester field with all options
2894    const $semesterSelect = jQuery('#nf-field-' + semesterID);
2895    $semesterSelect.find('option').remove();
2896    $semesterSelect.find('optgroup').remove();
2897    $semesterSelect.append('<option value="" label="">Choose Semester</option>');
2898    $semesterSelect.append(ninjaFormDataJS.semestersHTML);
2899
2900    // Set up change handlers for labels only (no filtering)
2901    jQuery("#nf-field-" + programID).change(function () {
2902        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2903        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2904
2905        // Handle campus data field if it exists
2906        if (typeof campusDataField !== 'undefined' && campusDataField) {
2907            const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2908            jQuery("#nf-field-" + campusDataField.id).val(dataSoloCampusID).trigger('change');
2909            if (typeof dataSoloCampusID !== 'undefined') {
2910                const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2911                    return ninjaFormDataJS.campusData[key];
2912                });
2913                const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2914                if (typeof soloCampus !== 'undefined') {
2915                    jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2916                }
2917            }
2918        }
2919        runRuleEngine();
2920    });
2921
2922    jQuery("#nf-field-" + campusID).change(function () {
2923        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2924        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2925        runRuleEngine();
2926    });
2927
2928    jQuery("#nf-field-" + semesterID).change(function () {
2929        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2930        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2931        runRuleEngine();
2932    });
2933
2934    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
2935        runRuleEngine();
2936    })
2937    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
2938        runRuleEngine();
2939    })
2940
2941    function runRuleEngine() {
2942
2943        const values = {
2944            program: jQuery("#nf-field-" + programID).val(),
2945            campus: jQuery("#nf-field-" + campusID).val(),
2946            semester: jQuery("#nf-field-" + semesterID).val(),
2947            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
2948            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
2949        };
2950
2951        const fieldIDs = {
2952            program: programID,
2953            campus: campusID,
2954            semester: semesterID,
2955            country: countryFieldId?.id,
2956            state: stateFieldId?.id
2957        };
2958
2959        // STEP 1: RESET everything first
2960        resetRuleEffects(fieldIDs);
2961
2962        // STEP 2: Apply rules fresh
2963        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
2964    }
2965
2966    // Since Program, Campus, and Semester are independent (non-chained) here,
2967    // auto-select each one on load if it only has a single available option.
2968    autoSelectSingleOption(programID);
2969    autoSelectSingleOption(campusID);
2970    autoSelectSingleOption(semesterID);
2971
2972    runRuleEngine(); // run once on load
2973}
2974
2975function selectProgramCampusUTM(programID, programLabelID, campusID, campusLabelID, campusTarget, utmCampus, utmProgram, dynamicFormType) {
2976    if (dynamicFormType === 'program' && typeof utmProgram !== 'undefined' && utmProgram.value) {
2977        selectProgramUTM(programID, programLabelID, utmProgram);
2978    }
2979    if (typeof campusTarget !== 'undefined') {
2980        const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2981        jQuery("#nf-field-" + campusTarget.id).val(dataSoloCampusID).trigger('change');
2982        if (typeof dataSoloCampusID !== 'undefined') {
2983            const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2984                return ninjaFormDataJS.campusData[key];
2985            });
2986            const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2987            if (typeof soloCampus !== 'undefined') {
2988                jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2989            }
2990        }
2991    }
2992    else if (utmCampus.value) {
2993        jQuery('#nf-field-' + campusID).val(utmCampus.value).trigger('change');
2994        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2995        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2996    }
2997    if (dynamicFormType !== 'program' && typeof utmProgram !== 'undefined' && utmProgram.value) {
2998        selectProgramUTM(programID, programLabelID, utmProgram);
2999    }
3000}
3001
3002function selectProgramUTM(programID, programLabelID, utmProgram) {
3003    if (utmProgram.value) {
3004        jQuery('#nf-field-' + programID).val(utmProgram.value).trigger('change');
3005        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
3006        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
3007    }
3008}
3009
3010function bindChangesPartChange(timezonefillID, submitButtonID) {
3011    const bindOnChangesOnPartChange = Marionette.Object.extend({
3012        initialize: function () {
3013            const fieldsChannel = Backbone.Radio.channel('fields');
3014            this.listenTo(fieldsChannel, 'change:model', this.fieldModelChange);
3015        },
3016        fieldModelChange: (fieldModel, formModel, conditionModel) => {
3017            if (fieldModel.changed.visible !== true) return;
3018            if ('time_zone_drop_down' == fieldModel.get('key')) {
3019                forumlateTimeZones(fieldModel.get('id'), fieldModel.get('value'), timezonefillID, submitButtonID);
3020            }
3021        }
3022    });
3023    new bindOnChangesOnPartChange();
3024}
3025
3026function forumlateTimeZones(modelID, modelValue, timezonefillID, submitButtonID) {
3027    jQuery("#nf-field-" + modelID).change(function () {
3028        if (modelValue) {
3029            const fieldValue = jQuery("#nf-field-" + modelID).val();
3030            jQuery("#nf-field-" + modelID).parent().append('<img src="' + ninjaFormDataJS.loadingGifPath + '" alt="tcspp-loader" class="timezone-loader" width="118" height="15" />');
3031            jQuery("#nf-field-" + modelID).prop('disabled', true);
3032            jQuery('#nf-field-' + submitButtonID).prop('disabled', true);
3033            jQuery.ajax({
3034                url: "https://maps.googleapis.com/maps/api/geocode/json?address=" + fieldValue + '&language=en' + "&key=" + ninjaFormDataJS.googleAPIKey,
3035                cache: false,
3036                dataType: "json",
3037                type: "GET",
3038                complete: function () {
3039                    jQuery(".timezone-loader").remove();
3040                    jQuery("#nf-field-" + modelID).prop('disabled', false);
3041                    jQuery('#nf-field-' + submitButtonID).prop('disabled', false);
3042                },
3043                success: function (result, success) {
3044                    if (success == 'success') {
3045                        const lat = result.results[0].geometry.location.lat;
3046                        const lng = result.results[0].geometry.location.lng;
3047                        const timestamp = Math.floor((new Date).getTime() / 1000);
3048                        jQuery.ajax({
3049                            url: "https://maps.googleapis.com/maps/api/timezone/json?location=" + lat + "," + lng + "&timestamp=" + timestamp + '&language=en' + "&key=" + ninjaFormDataJS.googleAPIKey,
3050                            cache: false,
3051                            dataType: "json",
3052                            type: "GET",
3053                            success: function (result, success) {
3054                                const dst = Number(result.dstOffset);
3055                                const raw = Number(result.rawOffset);
3056                                const adjustedTime = dst + raw;
3057                                const isNegative = Math.sign(adjustedTime);
3058                                const baseNums = Math.abs(adjustedTime);
3059                                if (baseNums > 0) {
3060                                    const timeinHours = secondsTimeSpanToHMS(baseNums);
3061                                    let appendDig = "";
3062                                    if (isNegative < 0) appendDig = '-';
3063                                    else appendDig = '+';
3064                                    const timeString = appendDig + timeinHours;
3065                                    jQuery("#nf-field-" + timezonefillID).val(timeString).trigger('change');
3066                                }
3067                            },
3068                            error: function (result, success) { }
3069                        });
3070                    }
3071                }
3072            });
3073        }
3074    });
3075}
3076
3077function secondsTimeSpanToHMS(seconds) {
3078    const hours = Math.floor(seconds / 3600);
3079    seconds -= hours * 3600;
3080    const minutes = Math.floor(seconds / 60);
3081    seconds -= minutes * 60;
3082    return (hours < 10 ? '0' + hours : hours) + ":" + (minutes < 10 ? '0' + minutes : minutes); //zero padding on minutes and seconds
3083}
3084
3085function setupAddressAutocomplete(countryFieldId) {
3086    jQuery(`#nf-field-${countryFieldId}`).prop('autocomplete', 'country');
3087    if (typeof jQuery(`#nf-field-${countryFieldId}`)[0] !== 'undefined') {
3088        clearInterval(autocompleteSetupInterval);
3089    }
3090}
3091// NOTE: useDynamicCampusProgram(form) now lives in one place, alongside the
3092// other form-field selector helpers above (see getFormDataSyncSetting).

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.