PageSourceSearch

https://www.saybrook.edu/wp-content/plugins/tcs-client-nf-sync/assets/js/tcsCoreScripts.js?ver=6.8.2

js saybrook.edu collected 2026-09-24 09:09:28 UTC 133,453 bytes, 3,102 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' && typeof submitButton !== '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 submitBtn = getSubmitButton(window.currentFormContext);
820        // Guard: some forms don't have a field whose key includes "submit",
821        // so getSubmitButton() can legitimately return undefined. Without
822        // this check, reading .id off it throws and aborts whatever called
823        // resetRuleEffects() (runRuleEngine(), called synchronously during
824        // form init), which in turn was silently preventing the
825        // Program/Campus "of Interest" preselect code later in the
826        // nfFormReady handler from ever running.
827        if (typeof submitBtn !== 'undefined' && submitBtn) {
828            const submitBtnId = submitBtn.id;
829            const $submitBtn = jQuery('#nf-field-' + submitBtnId);
830            $submitBtn.prop('disabled', false).removeClass('nf-disabled');
831            hideDO_NOT_SUBMIT_error(submitBtnId);
832        }
833    }
834}
835
836// ===========================================================================================================================
837
838// SCHOOL > PROGRAM > CAMPUS > SEMESTER chain (useDynamic === 'program')
839function schoolProgramCampusSemesterChain(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
840    // Initialize all dropdowns with appropriate default options
841    initializeDropdownWithDefault(schoolID, 'Choose School');
842    initializeDropdownWithDefault(programID, '--');
843    initializeDropdownWithDefault(campusID, '--');
844    initializeDropdownWithDefault(semesterID, '--');
845
846    // Initialize school field (no default option needed as it should show all schools)
847    loadSchoolOptions(schoolID, schoolLabelID);
848
849    // School -> Program filtering
850    jQuery("#nf-field-" + schoolID).change(function () {
851        const schoolValue = jQuery(this).val();
852
853        // Reset program dropdown with default option
854        initializeDropdownWithDefault(programID, 'Choose Program');
855
856        if (schoolValue) {
857            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
858
859            // Filter programs based on selected school - USING data-school-id
860            jQuery("#nf-field-" + programID + " option").each((index, option) => {
861                let optionSchoolId = jQuery(option).attr('data-school-id');
862
863                if (typeof optionSchoolId !== 'undefined' && schoolValue !== '' && option.value !== '') {
864                    // Compare the school ID directly
865                    if (optionSchoolId !== schoolValue) {
866                        jQuery(option).remove();
867                    }
868                }
869            });
870
871            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
872                if (jQuery(option).is(':empty')) {
873                    jQuery(option).remove();
874                }
875            });
876        }
877
878        // Reset dependent fields to default (Campus and Semester)
879        initializeDropdownWithDefault(campusID, '--');
880        initializeDropdownWithDefault(semesterID, '--');
881
882        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
883        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
884
885        // If only one Program remains available, auto-select it instead of
886        // requiring the user to choose it manually.
887        autoSelectSingleOption(programID);
888
889        runRuleEngine();
890    });
891
892    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
893    jQuery("#nf-field-" + programID).change(function () {
894        const programValue = jQuery(this).val();
895
896        // Reset campus dropdown with default option
897        initializeDropdownWithDefault(campusID, 'Choose Campus');
898
899        if (programValue) {
900            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
901            
902            // Try to filter using mapping, fallback to data-class if no mapping
903            filterCampusesByProgramMapping(programValue, campusID, programID);
904
905            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
906                if (jQuery(option).is(':empty')) {
907                    jQuery(option).remove();
908                }
909            });
910        }
911
912        // Reset dependent field (Semester) to default
913        initializeDropdownWithDefault(semesterID, '--');
914
915        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
916        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
917
918        // If only one Campus remains available, auto-select it instead of
919        // requiring the user to choose it manually.
920        autoSelectSingleOption(campusID);
921
922        runRuleEngine();
923    });
924
925    // Campus -> Semester filtering - FIXED VERSION
926    jQuery("#nf-field-" + campusID).change(function () {
927        const campusValue = jQuery(this).val();
928        const programValue = jQuery("#nf-field-" + programID).val(); // Get current program value
929
930        // Reset semester dropdown with default option
931        initializeDropdownWithDefault(semesterID, 'Choose Semester');
932
933        if (campusValue && programValue) {
934            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
935
936            // Filter semesters based on selected program AND campus
937            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
938                let optionSemesters = jQuery(option).attr('data-class');
939
940                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
941                    // The data-class contains pairs like "67-104 67-802"
942                    // We need to check if any pair matches the selected program and campus
943                    const semesterPairs = optionSemesters.split(' ');
944                    let shouldKeep = false;
945
946                    semesterPairs.forEach(pair => {
947                        const [programId, campusId] = pair.split('-');
948                        if (programId === programValue && campusId === campusValue) {
949                            shouldKeep = true;
950                        }
951                    });
952
953                    if (!shouldKeep && option.value !== '') {
954                        jQuery(option).remove();
955                    }
956                }
957            });
958
959            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
960                if (jQuery(option).is(':empty')) {
961                    jQuery(option).remove();
962                }
963            });
964
965            singleSemesterSelect(semesterID, campusID);
966        }
967
968        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
969        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
970        runRuleEngine();
971    });
972
973    // Semester change handler
974    jQuery("#nf-field-" + semesterID).change(function () {
975        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
976        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
977        runRuleEngine();
978    });
979
980    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
981        runRuleEngine();
982    })
983    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
984        runRuleEngine();
985    })
986
987    function runRuleEngine() {
988
989        const values = {
990            school: jQuery("#nf-field-" + schoolID).val(),
991            program: jQuery("#nf-field-" + programID).val(),
992            campus: jQuery("#nf-field-" + campusID).val(),
993            semester: jQuery("#nf-field-" + semesterID).val(),
994            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
995            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
996        };
997
998        const fieldIDs = {
999            school: schoolID,
1000            program: programID,
1001            campus: campusID,
1002            semester: semesterID,
1003            country: countryFieldId?.id,
1004            state: stateFieldId?.id
1005        };
1006
1007        // STEP 1: RESET everything first
1008        resetRuleEffects(fieldIDs);
1009
1010        // STEP 2: Apply rules fresh
1011        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1012    }
1013
1014    runRuleEngine(); // run once on load
1015}
1016
1017// =========================================================================================================================================================================================
1018
1019// SCHOOL > CAMPUS > PROGRAM > SEMESTER chain (useDynamic === 'campus')
1020function schoolCampusProgramSemesterChain(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1021    // Initialize all dropdowns with appropriate default options
1022    initializeDropdownWithDefault(schoolID, 'Choose School');
1023    initializeDropdownWithDefault(programID, '--');
1024    initializeDropdownWithDefault(campusID, '--');
1025    initializeDropdownWithDefault(semesterID, '--');
1026
1027    // Initialize school field
1028    loadSchoolOptions(schoolID, schoolLabelID);
1029
1030    // School -> Campus filtering
1031    jQuery("#nf-field-" + schoolID).change(function () {
1032        const schoolValue = jQuery(this).val();
1033
1034        // Reset campus dropdown with default option
1035        initializeDropdownWithDefault(campusID, 'Choose Campus');
1036
1037        if (schoolValue) {
1038            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1039
1040            // Filter campuses based on selected school - USING data-school-id
1041            jQuery("#nf-field-" + campusID + " option").each((index, option) => {
1042                let optionSchoolId = jQuery(option).attr('data-school-id');
1043
1044                if (typeof optionSchoolId !== 'undefined' && schoolValue !== '' && option.value !== '') {
1045                    // Compare the school ID directly
1046                    if (optionSchoolId !== schoolValue) {
1047                        jQuery(option).remove();
1048                    }
1049                }
1050            });
1051
1052            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1053                if (jQuery(option).is(':empty')) {
1054                    jQuery(option).remove();
1055                }
1056            });
1057        }
1058
1059        // Reset dependent fields to default (Program and Semester)
1060        initializeDropdownWithDefault(programID, '--');
1061        initializeDropdownWithDefault(semesterID, '--');
1062
1063        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1064        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1065
1066        // If only one Campus remains available, auto-select it instead of
1067        // requiring the user to choose it manually.
1068        autoSelectSingleOption(campusID);
1069
1070        runRuleEngine();
1071    });
1072
1073    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1074    jQuery("#nf-field-" + campusID).change(function () {
1075        const campusValue = jQuery(this).val();
1076
1077        // Reset program dropdown with default option
1078        initializeDropdownWithDefault(programID, 'Choose Program');
1079
1080        if (campusValue) {
1081            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1082            
1083            // Try to filter programs using mapping
1084            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1085            
1086            // If no mapping was applied, show all programs
1087            if (!mappingApplied) {
1088                // Filter programs based on selected campus
1089                jQuery("#nf-field-" + programID + " option").each((index, option) => {
1090                    let optionCampuses = jQuery(option).attr('data-class');
1091                    if (typeof optionCampuses !== 'undefined' && campusValue !== '') {
1092                        optionCampuses = optionCampuses.split(' ');
1093                        if (optionCampuses.indexOf(campusValue) === -1 && option.value !== '') {
1094                            jQuery(option).remove();
1095                        }
1096                    }
1097                });
1098            }
1099
1100            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1101                if (jQuery(option).is(':empty')) {
1102                    jQuery(option).remove();
1103                }
1104            });
1105        }
1106
1107        // Reset dependent field (Semester) to default
1108        initializeDropdownWithDefault(semesterID, '--');
1109
1110        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1111        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1112
1113        // If only one Program remains available, auto-select it instead of
1114        // requiring the user to choose it manually.
1115        autoSelectSingleOption(programID);
1116
1117        runRuleEngine();
1118    });
1119
1120    // Program -> Semester filtering
1121    jQuery("#nf-field-" + programID).change(function () {
1122        const programValue = jQuery(this).val();
1123        const campusValue = jQuery("#nf-field-" + campusID).val();
1124
1125        // Reset semester dropdown with default option
1126        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1127
1128        if (programValue && campusValue) {
1129            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1130
1131            // Filter semesters based on selected program AND campus
1132            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1133                let optionSemesters = jQuery(option).attr('data-class');
1134                let optionPrograms = jQuery(option).attr('data-program-solo-id');
1135
1136                let shouldRemove = false;
1137
1138                // Filter by campus and program using data-class (format: "program-campus" pairs)
1139                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1140                    const semesterPairs = optionSemesters.split(' ');
1141                    let hasMatchingPair = false;
1142
1143                    semesterPairs.forEach(pair => {
1144                        const [programId, campusId] = pair.split('-');
1145                        if (programId === programValue && campusId === campusValue) {
1146                            hasMatchingPair = true;
1147                        }
1148                    });
1149
1150                    if (!hasMatchingPair && option.value !== '') {
1151                        shouldRemove = true;
1152                    }
1153                }
1154
1155                // Additional filter by program using data-program-solo-id
1156                if (!shouldRemove && typeof optionPrograms !== 'undefined' && programValue !== '') {
1157                    optionPrograms = optionPrograms.split(' ');
1158                    if (optionPrograms.indexOf(programValue) === -1 && option.value !== '') {
1159                        shouldRemove = true;
1160                    }
1161                }
1162
1163                if (shouldRemove) {
1164                    jQuery(option).remove();
1165                }
1166            });
1167
1168            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1169                if (jQuery(option).is(':empty')) {
1170                    jQuery(option).remove();
1171                }
1172            });
1173
1174            singleSemesterSelect(semesterID, campusID);
1175        }
1176
1177        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1178        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1179        runRuleEngine();
1180    });
1181
1182    // Semester change handler
1183    jQuery("#nf-field-" + semesterID).change(function () {
1184        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1185        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1186        runRuleEngine();
1187    });
1188
1189    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1190        runRuleEngine();
1191    })
1192    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1193        runRuleEngine();
1194    })
1195
1196    function runRuleEngine() {
1197
1198        const values = {
1199            school: jQuery("#nf-field-" + schoolID).val(),
1200            program: jQuery("#nf-field-" + programID).val(),
1201            campus: jQuery("#nf-field-" + campusID).val(),
1202            semester: jQuery("#nf-field-" + semesterID).val(),
1203            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1204            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1205        };
1206
1207        const fieldIDs = {
1208            school: schoolID,
1209            program: programID,
1210            campus: campusID,
1211            semester: semesterID,
1212            country: countryFieldId?.id,
1213            state: stateFieldId?.id
1214        };
1215
1216        // STEP 1: RESET everything first
1217        resetRuleEffects(fieldIDs);
1218
1219        // STEP 2: Apply rules fresh
1220        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1221    }
1222
1223    runRuleEngine(); // run once on load
1224}
1225
1226// PROGRAM > CAMPUS > SEMESTER chain (no school, useDynamic === 'program') - UPDATED
1227function programCampusSemesterChainNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1228    console.log('programCampusSemesterChainNoSchool');
1229
1230    // Initialize program field with all options
1231    const $programSelect = jQuery('#nf-field-' + programID);
1232    $programSelect.find('option').remove();
1233    $programSelect.find('optgroup').remove();
1234    $programSelect.append('<option value="" label="">Choose Program</option>');
1235    $programSelect.append(ninjaFormDataJS.programsHTML);
1236
1237    // Initialize other fields with default options
1238    initializeDropdownWithDefault(campusID, '--');
1239    initializeDropdownWithDefault(semesterID, '--');
1240
1241    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
1242    jQuery("#nf-field-" + programID).change(function () {
1243        const programValue = jQuery(this).val();
1244
1245        // Reset campus dropdown with default option
1246        initializeDropdownWithDefault(campusID, 'Choose Campus');
1247        // Reset semester dropdown with default option
1248        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1249
1250        if (programValue) {
1251            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1252
1253            // Try to filter using mapping, fallback to data-class if no mapping
1254            filterCampusesByProgramMapping(programValue, campusID, programID);
1255
1256            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1257                if (jQuery(option).is(':empty')) {
1258                    jQuery(option).remove();
1259                }
1260            });
1261
1262            // Populate Semester filtered by the selected Program alone
1263            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1264
1265            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1266                if (option.value === '') return;
1267                const optionPrograms = (jQuery(option).attr('data-program-solo-id') || '').split(' ').filter(
1267Boolean);
1268                if (optionPrograms.indexOf(programValue) === -1) {
1269                    jQuery(option).remove();
1270                }
1271            });
1272
1273            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1274                if (jQuery(option).is(':empty')) {
1275                    jQuery(option).remove();
1276                }
1277            });
1278        }
1279
1280        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1281        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1282
1283        // If only one Campus remains available, auto-select it instead of
1284        // requiring the user to choose it manually.
1285        autoSelectSingleOption(campusID);
1286
1287        runRuleEngine();
1288    });
1289
1290    // Campus -> Semester filtering
1291    jQuery("#nf-field-" + campusID).change(function () {
1292        const campusValue = jQuery(this).val();
1293        const programValue = jQuery("#nf-field-" + programID).val(); // Get current program value
1294
1295        // Reset semester dropdown with default option
1296        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1297
1298        if (campusValue && programValue) {
1299            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1300
1301            // Filter semesters based on selected campus and program
1302            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1303                let optionSemesters = jQuery(option).attr('data-class');
1304
1305                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1306                    // The data-class contains pairs like "67-104 67-802"
1307                    // We need to check if any pair matches the selected program and campus
1308                    const semesterPairs = optionSemesters.split(' ');
1309                    let shouldKeep = false;
1310
1311                    semesterPairs.forEach(pair => {
1312                        const [programId, campusId] = pair.split('-');
1313                        if (programId === programValue && campusId === campusValue) {
1314                            shouldKeep = true;
1315                        }
1316                    });
1317
1318                    if (!shouldKeep && option.value !== '') {
1319                        jQuery(option).remove();
1320                    }
1321                }
1322            });
1323
1324            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1325                if (jQuery(option).is(':empty')) {
1326                    jQuery(option).remove();
1327                }
1328            });
1329
1330            singleSemesterSelect(semesterID, campusID);
1331        }
1332
1333        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1334        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1335        runRuleEngine();
1336    });
1337
1338    // Semester change handler
1339    jQuery("#nf-field-" + semesterID).change(function () {
1340        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1341        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1342        runRuleEngine();
1343    });
1344
1345    // Program label change handler
1346    jQuery("#nf-field-" + programID).change(function () {
1347        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1348        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1349        runRuleEngine();
1350    });
1351
1352    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1353        runRuleEngine();
1354    })
1355    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1356        runRuleEngine();
1357    })
1358
1359    function runRuleEngine() {
1360        console.log('runRuleEngine');
1361
1362        const values = {
1363            program: jQuery("#nf-field-" + programID).val(),
1364            campus: jQuery("#nf-field-" + campusID).val(),
1365            semester: jQuery("#nf-field-" + semesterID).val(),
1366            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1367            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1368        };
1369
1370        sessionStorage.setItem("nf_program", values.program || "");
1371        sessionStorage.setItem("nf_campus", values.campus || "");
1372        sessionStorage.setItem("nf_semester", values.semester || "");
1373        sessionStorage.setItem("nf_country", values.country || "");
1374        sessionStorage.setItem("nf_state", values.state || "");
1375
1376        const fieldIDs = {
1377            program: programID,
1378            campus: campusID,
1379            semester: semesterID,
1380            country: countryFieldId?.id,
1381            state: stateFieldId?.id
1382        };
1383
1384        // STEP 1: RESET everything first
1385        resetRuleEffects(fieldIDs);
1386
1387        // STEP 2: Apply rules fresh
1388        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1389    }
1390
1391    // If only one Program is available on load, auto-select it. This cascades
1392    // (via the change handlers above) to auto-select Campus and Semester too,
1393    // when each of those also only has a single available option.
1394    autoSelectSingleOption(programID);
1395
1396    runRuleEngine(); // run once on load
1397}
1398
1399// Non-chained dropdown behavior (no school, useDynamic === '')
1400// CAMPUS > PROGRAM > SEMESTER chain (no school, useDynamic === 'campus') - UPDATED
1401function campusProgramSemesterChainNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1402    // Initialize campus field with "Choose Campus" on page load
1403    const $campusSelect = jQuery('#nf-field-' + campusID);
1404    $campusSelect.find('option').remove();
1405    $campusSelect.find('optgroup').remove();
1406    $campusSelect.append('<option value="" label="">Choose Campus</option>');
1407    $campusSelect.append(ninjaFormDataJS.campusesHTML);
1408
1409    // Initialize program field with "--" on page load
1410    initializeDropdownWithDefault(programID, '--');
1411
1412    // Initialize semester field with "--" on page load
1413    initializeDropdownWithDefault(semesterID, '--');
1414
1415    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1416    jQuery("#nf-field-" + campusID).change(function () {
1417        const campusValue = jQuery(this).val();
1418
1419        // Reset program dropdown with "Choose Program" after campus selection
1420        initializeDropdownWithDefault(programID, 'Choose Program');
1421
1422        if (campusValue) {
1423            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1424
1425            // Try to filter programs using mapping
1426            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1427            
1428            // If no mapping was applied, show all programs
1429            if (!mappingApplied) {
1430                // Filter programs based on selected campus
1431                    jQuery("#nf-field-" + programID + " option").each((index, option) => {
1432                    let optionCampuses = jQuery(option).attr('data-class');
1433                    if (typeof optionCampuses !== 'undefined' && campusValue !== '') {
1434                        optionCampuses = optionCampuses.split(' ');
1435                        if (optionCampuses.indexOf(campusValue) === -1 && option.value !== '') {
1436                            jQuery(option).remove();
1437                        }
1438                    }
1439                });
1440            }
1441
1442            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1443                if (jQuery(option).is(':empty')) {
1444                    jQuery(option).remove();
1445                }
1446            });
1447        }
1448
1449        // Reset dependent field (Semester) to "--"
1450        initializeDropdownWithDefault(semesterID, '--');
1451
1452        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1453        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1454
1455        // If only one Program remains available, auto-select it instead of
1456        // requiring the user to choose it manually.
1457        autoSelectSingleOption(programID);
1458
1459        runRuleEngine();
1460    });
1461
1462    // Program -> Semester filtering
1463    jQuery("#nf-field-" + programID).change(function () {
1464        const programValue = jQuery(this).val();
1465        const campusValue = jQuery("#nf-field-" + campusID).val();
1466
1467        // Reset semester dropdown with "Choose Semester" after program selection
1468        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1469
1470        if (programValue && campusValue) {
1471            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1472
1473            // Filter semesters based on selected program AND campus
1474            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1475                let optionSemesters = jQuery(option).attr('data-class');
1476                let optionPrograms = jQuery(option).attr('data-program-solo-id');
1477
1478                let shouldRemove = false;
1479
1480                // Filter by campus and program using data-class (format: "program-campus" pairs)
1481                if (typeof optionSemesters !== 'undefined' && campusValue !== '' && programValue !== '') {
1482                    const semesterPairs = optionSemesters.split(' ');
1483                    let hasMatchingPair = false;
1484
1485                    semesterPairs.forEach(pair => {
1486                        const [programId, campusId] = pair.split('-');
1487                        if (programId === programValue && campusId === campusValue) {
1488                            hasMatchingPair = true;
1489                        }
1490                    });
1491
1492                    if (!hasMatchingPair && option.value !== '') {
1493                        shouldRemove = true;
1494                    }
1495                }
1496
1497                // Additional filter by program using data-program-solo-id
1498                if (!shouldRemove && typeof optionPrograms !== 'undefined' && programValue !== '') {
1499                    optionPrograms = optionPrograms.split(' ');
1500                    if (optionPrograms.indexOf(programValue) === -1 && option.value !== '') {
1501                        shouldRemove = true;
1502                    }
1503                }
1504
1505                if (shouldRemove) {
1506                    jQuery(option).remove();
1507                }
1508            });
1509
1510            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1511                if (jQuery(option).is(':empty')) {
1512                    jQuery(option).remove();
1513                }
1514            });
1515
1516            singleSemesterSelect(semesterID, campusID);
1517        }
1518
1519        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1520        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1521        runRuleEngine();
1522    });
1523
1524    // Semester change handler
1525    jQuery("#nf-field-" + semesterID).change(function () {
1526        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1527        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1528        runRuleEngine();
1529    });
1530
1531    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1532        runRuleEngine();
1533    })
1534    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1535        runRuleEngine();
1536    })
1537
1538    function runRuleEngine() {
1539
1540        const values = {
1541            program: jQuery("#nf-field-" + programID).val(),
1542            campus: jQuery("#nf-field-" + campusID).val(),
1543            semester: jQuery("#nf-field-" + semesterID).val(),
1544            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1545            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1546        };
1547
1548        const fieldIDs = {
1549            program: programID,
1550            campus: campusID,
1551            semester: semesterID,
1552            country: countryFieldId?.id,
1553            state: stateFieldId?.id
1554        };
1555
1556        // STEP 1: RESET everything first
1557        resetRuleEffects(fieldIDs);
1558
1559        // STEP 2: Apply rules fresh
1560        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1561    }
1562
1563    // If only one Campus is available on load, auto-select it. This cascades
1564    // (via the change handlers above) to auto-select Program and Semester too,
1565    // when each of those also only has a single available option.
1566    autoSelectSingleOption(campusID);
1567
1568    runRuleEngine(); // run once on load
1569}
1570
1571function programCampusSemesterChain(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1572    // Initialize dropdowns with defaults
1573    initializeDropdownWithDefault(campusID, 'Choose Campus');
1574    initializeDropdownWithDefault(semesterID, 'Choose Semester');
1575
1576    // Program -> Campus filtering - UPDATED to use program_campus_mapping with fallback
1577    programCampusDropDowns(programID, programLabelID, campusID, campusLabelID);
1578
1579    // Campus -> Semester filtering
1580    campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID);
1581}
1582
1583// CAMPUS > PROGRAM > SEMESTER chain (no school, useDynamic === 'campus')
1584function campusProgramSemesterChain(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1585    // Initialize dropdowns with defaults
1586    initializeDropdownWithDefault(programID, 'Choose Program');
1587    initializeDropdownWithDefault(semesterID, 'Choose Semester');
1588
1589    // Campus -> Program filtering - UPDATED to use program_campus_mapping
1590    campusProgramDropDowns(programID, programLabelID, campusID, campusLabelID);
1591
1592    // Campus -> Semester filtering
1593    campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID);
1594}
1595
1596// UPDATED: Program -> Campus filtering with program_campus_mapping and fallback
1597function programCampusDropDowns(programID, programLabelID, campusID, campusLabelID) {
1598    // Remove any empty data-solo-campus-id options
1599    jQuery(`#nf-field-${programID} option[data-solo-campus-id=""]`).remove();
1600
1601    jQuery("#nf-field-" + programID).change(() => {
1602        const programValue = jQuery("#nf-field-" + programID).val();
1603
1604        // Reset campus dropdown with default option
1605        initializeDropdownWithDefault(campusID, 'Choose Campus');
1606
1607        if (programValue) {
1608            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1609
1610            // Try to filter using mapping, fallback to data-class if no mapping
1611            filterCampusesByProgramMapping(programValue, campusID, programID);
1612
1613            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1614                if (jQuery(option).is(':empty')) {
1615                    jQuery(option).remove();
1616                }
1617            });
1618        }
1619
1620        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1621        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1622    });
1623
1624    // Campus change handler for label updates
1625    jQuery("#nf-field-" + campusID).change(() => {
1626        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1627        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1628    });
1629}
1630
1631// UPDATED: Campus -> Program filtering with program_campus_mapping
1632function campusProgramDropDowns(programID, programLabelID, campusID, campusLabelID) {
1633    jQuery("#nf-field-" + campusID).change(() => {
1634        const campusValue = jQuery("#nf-field-" + campusID).val();
1635
1636        // Reset program dropdown with default option
1637        initializeDropdownWithDefault(programID, 'Choose Program');
1638
1639        if (campusValue) {
1640            jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1641
1642            // Try to filter programs using mapping
1643            const mappingApplied = filterProgramsByCampusMapping(campusValue, programID);
1644            
1645            // If no mapping was applied, show all programs
1646            if (!mappingApplied) {
1647                jQuery("#nf-field-" + programID + " option").show();
1648                jQuery("#nf-field-" + programID + " option").prop('disabled', false);
1649            }
1650
1651            jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1652                if (jQuery(option).is(':empty')) {
1653                    jQuery(option).remove();
1654                }
1655            });
1656        }
1657
1658        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1659        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1660    });
1661
1662    jQuery("#nf-field-" + programID).change(() => {
1663        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1664        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1665    });
1666}
1667
1668// UPDATED FUNCTION: Campus -> Semester filtering (standalone)
1669function campusSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID) {
1670    jQuery("#nf-field-" + campusID).change(function () {
1671        const campusValue = jQuery(this).val();
1672
1673        // Reset and populate semester dropdown with default
1674        initializeDropdownWithDefault(semesterID, 'Choose Semester');
1675
1676        if (campusValue) {
1677            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1678
1679            // Filter semesters based on selected campus
1680            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1681                let optionSemesters = jQuery(option).attr('data-class');
1682
1683                if (typeof optionSemesters !== 'undefined' && campusValue !== '') {
1684                    optionSemesters = optionSemesters.split(' ');
1685                    if (optionSemesters.indexOf(campusValue) === -1 && option.value !== '') {
1686                        jQuery(option).remove();
1687                    }
1688                }
1689            });
1690
1691            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1692                if (jQuery(option).is(':empty')) {
1693                    jQuery(option).remove();
1694                }
1695            });
1696
1697            singleSemesterSelect(semesterID, campusID);
1698        }
1699
1700        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1701        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1702    });
1703
1704    // Semester change handler
1705    jQuery("#nf-field-" + semesterID).change(function () {
1706        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1707        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1708    });
1709}
1710
1711// Add the missing selector functions
1712function getUtmSchoolField(form) {
1713    return form.fields.find(f => f.key === 'utm_school');
1714}
1715
1716// Add the missing school initialization function for standalone school fields
1717function initializeSchoolField(schoolID, schoolLabelID) {
1718    jQuery("#nf-field-" + schoolID).change(function () {
1719        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1720        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1721    });
1722
1723    // Set initial value if there's a selected option
1724    const selectedSchool = jQuery("#nf-field-" + schoolID + " option:selected");
1725    if (selectedSchool.length > 0 && selectedSchool.val() !== '') {
1726        const initialSchoolLabel = selectedSchool.text();
1727        jQuery("#nf-field-" + schoolLabelID).val(initialSchoolLabel).trigger('change');
1728    }
1729}
1730
1731// Add the missing UTM function for schools
1732function selectSchoolUTM(schoolID, schoolLabelID, utmSchool) {
1733    if (utmSchool && utmSchool.value) {
1734        jQuery('#nf-field-' + schoolID).val(utmSchool.value).trigger('change');
1735        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1736        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1737    }
1738}
1739
1740// UPDATED FUNCTION: Directly load school options
1741function loadSchoolOptions(schoolID, schoolLabelID) {
1742
1743    const $schoolSelect = jQuery('#nf-field-' + schoolID);
1744    if (!$schoolSelect.length) {
1745        console.error('School select element not found: #nf-field-' + schoolID);
1746        return;
1747    }
1748
1749    // Clear existing options
1750    $schoolSelect.find('option').remove();
1751    $schoolSelect.find('optgroup').remove();
1752
1753    // Add default option first
1754    $schoolSelect.append('<option value="" label="">Choose School</option>');
1755
1756    // Append the schoolsHTML that's now available from PHP
1757    if (typeof ninjaFormDataJS.schoolsHTML !== 'undefined' && ninjaFormDataJS.schoolsHTML) {
1758        $schoolSelect.append(ninjaFormDataJS.schoolsHTML);
1759    } else {
1760        console.warn('ninjaFormDataJS.schoolsHTML is undefined');
1761    }
1762
1763    // Initialize the change handler
1764    initializeSchoolField(schoolID, schoolLabelID);
1765}
1766
1767// Add non-chained behavior for school forms
1768// Non-chained dropdown behavior for school forms (useDynamic === '')
1769function nonChainedSchoolDropDowns(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, countryFieldId, stateFieldId) {
1770    // Initialize all dropdowns with their full options including defaults
1771    loadSchoolOptions(schoolID, schoolLabelID); // This now includes "Choose School"
1772
1773    // Initialize program field with all options
1774    const $programSelect = jQuery('#nf-field-' + programID);
1775    $programSelect.find('option').remove();
1776    $programSelect.find('optgroup').remove();
1777    $programSelect.append('<option value="" label="">Choose Program</option>');
1778    $programSelect.append(ninjaFormDataJS.programsHTML);
1779
1780    // Initialize campus field with all options  
1781    const $campusSelect = jQuery('#nf-field-' + campusID);
1782    $campusSelect.find('option').remove();
1783    $campusSelect.find('optgroup').remove();
1784    $campusSelect.append('<option value="" label="">Choose Campus</option>');
1785    $campusSelect.append(ninjaFormDataJS.campusesHTML);
1786
1787    // Initialize semester field with all options
1788    const $semesterSelect = jQuery('#nf-field-' + semesterID);
1789    $semesterSelect.find('option').remove();
1790    $semesterSelect.find('optgroup').remove();
1791    $semesterSelect.append('<option value="" label="">Choose Semester</option>');
1792    $semesterSelect.append(ninjaFormDataJS.semestersHTML);
1793
1794    // Set up change handlers for labels only (no filtering)
1795    jQuery("#nf-field-" + schoolID).change(function () {
1796        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1797        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1798        runRuleEngine();
1799    });
1800
1801    jQuery("#nf-field-" + programID).change(function () {
1802        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1803        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1804        runRuleEngine();
1805    });
1806
1807    jQuery("#nf-field-" + campusID).change(function () {
1808        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1809        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1810        runRuleEngine();
1811    });
1812
1813    jQuery("#nf-field-" + semesterID).change(function () {
1814        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1815        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1816        runRuleEngine();
1817    });
1818
1819    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
1820        runRuleEngine();
1821    })
1822    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
1823        runRuleEngine();
1824    })
1825
1826    function runRuleEngine() {
1827
1828        const values = {
1829            school: jQuery("#nf-field-" + schoolID).val(),
1830            program: jQuery("#nf-field-" + programID).val(),
1831            campus: jQuery("#nf-field-" + campusID).val(),
1832            semester: jQuery("#nf-field-" + semesterID).val(),
1833            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
1834            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
1835        };
1836
1837        const fieldIDs = {
1838            school: schoolID,
1839            program: programID,
1840            campus: campusID,
1841            semester: semesterID,
1842            country: countryFieldId?.id,
1843            state: stateFieldId?.id
1844        };
1845
1846        // STEP 1: RESET everything first
1847        resetRuleEffects(fieldIDs);
1848
1849        // STEP 2: Apply rules fresh
1850        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
1851    }
1852
1853    // Since Program, Campus, and Semester are independent (non-chained) here,
1854    // auto-select each one on load if it only has a single available option.
1855    autoSelectSingleOption(programID);
1856    autoSelectSingleOption(campusID);
1857    autoSelectSingleOption(semesterID);
1858
1859    runRuleEngine(); // run once on load
1860}
1861
1862// Complete school > program > campus > semester chaining function
1863function schoolProgramCampusDropDowns(schoolID, schoolLabelID, programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID) {
1864    // Initialize school field
1865    initializeSchoolField(schoolID, schoolLabelID);
1866
1867    // When school changes, filter programs
1868    jQuery("#nf-field-" + schoolID).change(function () {
1869        const schoolValue = jQuery(this).val();
1870
1871        // Reset and populate program dropdown
1872        jQuery("#nf-field-" + programID + " option").remove();
1873        jQuery("#nf-field-" + programID + " optgroup").remove();
1874        jQuery("#nf-field-" + programID).append('<option value="" label="">Choose Program</option>');
1875        jQuery("#nf-field-" + programID).append(ninjaFormDataJS.programsHTML);
1876
1877        // Filter programs based on selected school
1878        jQuery("#nf-field-" + programID + " option").each((index, option) => {
1879            let optionSchools = jQuery(option).attr('data-schools');
1880            if (typeof optionSchools !== 'undefined' && schoolValue !== '') {
1881                optionSchools = optionSchools.split(' ');
1882                if (optionSchools.indexOf(schoolValue) === -1 && option.value !== '') {
1883                    jQuery(option).remove();
1884                }
1885            }
1886        });
1887
1888        jQuery("#nf-field-" + programID + " optgroup").each((index, option) => {
1889            if (jQuery(option).is(':empty')) {
1890                jQuery(option).remove();
1891            }
1892        });
1893
1894        // Trigger program change to filter campuses
1895        jQuery("#nf-field-" + programID).trigger('change');
1896
1897        const schoolLabel = jQuery("#nf-field-" + schoolID + " option:selected").text();
1898        jQuery("#nf-field-" + schoolLabelID).val(schoolLabel).trigger('change');
1899    });
1900
1901    // Program change handler - UPDATED to use program_campus_mapping with fallback
1902    jQuery("#nf-field-" + programID).change(function () {
1903        const programValue = jQuery(this).val();
1904
1905        // Reset and populate campus dropdown
1906        jQuery("#nf-field-" + campusID + " option").remove();
1907        jQuery("#nf-field-" + campusID + " optgroup").remove();
1908        jQuery("#nf-field-" + campusID).append('<option value="" label="">Choose Campus</option>');
1909        jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
1910
1911        if (programValue) {
1912            // Try to filter using mapping, fallback to data-class if no mapping
1913            filterCampusesByProgramMapping(programValue, campusID, programID);
1914        }
1915
1916        jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
1917            if (jQuery(option).is(':empty')) {
1918                jQuery(option).remove();
1919            }
1920        });
1921
1922        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
1923        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
1924
1925        // Trigger campus change to filter semesters
1926        jQuery("#nf-field-" + campusID).trigger('change');
1927    });
1928
1929    // Campus change handler
1930    jQuery("#nf-field-" + campusID).change(function () {
1931        const campusValue = jQuery(this).val();
1932
1933        // Reset and populate semester dropdown
1934        jQuery("#nf-field-" + semesterID + " option").remove();
1935        jQuery("#nf-field-" + semesterID + " optgroup").remove();
1936        jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
1937        jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
1938
1939        // Filter semesters based on selected campus
1940        jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
1941            let optionSemesters = jQuery(option).attr('data-class');
1942
1943            if (typeof optionSemesters !== 'undefined' && campusValue !== '') {
1944                optionSemesters = optionSemesters.split(' ');
1945                if (optionSemesters.indexOf(campusValue) === -1 && option.value !== '') {
1946                    jQuery(option).remove();
1947                }
1948            }
1949        });
1950
1951        jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
1952            if (jQuery(option).is(':empty')) {
1953                jQuery(option).remove();
1954            }
1955        });
1956
1957        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
1958        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
1959
1960        singleSemesterSelect(semesterID, campusID);
1961    });
1962
1963    // Semester change handler
1964    jQuery("#nf-field-" + semesterID).change(function () {
1965        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
1966        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
1967    });
1968}
1969
1970//selector functions
1971
1972function getPageUrlField(form) {
1973    return form.fields.find(f => f.key === 'page_url');
1974}
1975
1976function getThankYouPageField(form) {
1977    return form.fields.find(f => f.key === 'thank_you_page');
1978}
1979
1980function getSubmitButton(form) {
1981    return form.fields.find(f => f.key.includes('submit'));
1982}
1983
1984function getTimezoneField(form) {
1985    return form.fields.find(f => f.key === 'selected_timezone');
1986}
1987
1988function getProgramSelectField(form) {
1989    return form.fields.find(f => f.key === 'program_select');
1990}
1991
1992function getCampusSelectField(form) {
1993    return form.fields.find(f => f.key === 'campus_select');
1994}
1995
1996function getCampusDataField(form) {
1997    return form.fields.find(f => f.key === 'use_campus_data_attribute');
1998}
1999
2000function getUtmCampusField(form) {
2001    return form.fields.find(f => f.key === 'utm_campus');
2002}
2003
2004// School Field Functions
2005function getSchoolSelectField(form) {
2006    return form.fields.find(f => f.key === 'school_select');
2007}
2008
2009function getSchoolLabelField(form) {
2010    return form.fields.find(f => f.key === 'school_label');
2011}
2012
2013function getCampusLabelField(form) {
2014    return form.fields.find(f => f.key === 'campus_label');
2015}
2016
2017function getUtmProgramField(form) {
2018    return form.fields.find(f => f.key === 'utm_program');
2019}
2020
2021function getProgramLabelField(form) {
2022    return form.fields.find(f => f.key === 'program_label');
2023}
2024
2025function getSemesterSelectField(form) {
2026    return form.fields.find(f => f.key === 'semester_select');
2027}
2028
2029function getSemesterLabelField(form) {
2030    return form.fields.find(f => f.key === 'semester_label');
2031}
2032
2033function getSrcParamTranslatedField(form) {
2034    return form.fields.find(f => f.key === 'src_parameter_translated');
2035}
2036
2037function getCountryZipTypeField(form) {
2038    return form.fields.find(f => f.key === 'country_zip_type');
2039}
2040
2041function getZipStatusField(form) {
2042    return form.fields.find(f => f.key === 'zip_status');
2043}
2044
2045function getPostalCodeField(form) {
2046    return form.fields.find(f => f.key === 'postal_code');
2047}
2048
2049function getZipCodeField(form) {
2050    return form.fields.find(f => f.key === 'zip_code');
2051}
2052
2053function getCountryField(form) {
2054    return form.fields.find(f => f.key === 'list_country_main');
2055}
2056
2057function getCityField(form) {
2058    return form.fields.find(f => f.key === 'visible_city');
2059}
2060
2061function getStateField(form) {
2062    return form.fields.find(f => f.key === 'visible_state');
2063}
2064
2065function getProvinceField(form) {
2066    return form.fields.find(f => f.key === 'province_ca');
2067}
2068
2069function getInternationalStateField(form) {
2070    return form.fields.find(f => f.key === 'int_state');
2071}
2072
2073function getLocalLatField(form) {
2074    return form.fields.find(f => f.key === 'local_latitude');
2075}
2076
2077function getLocalLongField(form) {
2078    return form.fields.find(f => f.key === 'local_longitude');
2079}
2080
2081function getCountryCodeField(form) {
2082    return form.fields.find(f =>
2082 f.key === 'gdpr_country_code');
2083}
2084
2085function getEmailField(form) {
2086    return form.fields.find(f => f.type === 'email');
2087}
2088
2089function getDateLimit(form) {
2090    const i = ninjaFormDataJS.ninjaFormsIDs.indexOf(form.id);
2091    if (ninjaFormDataJS.ninjaDateSettings[i] > 0) return Number(ninjaFormDataJS.ninjaDateSettings[i]);
2092    else return 0;
2093}
2094
2095// Looks up this form's per-form "Data Sync" setting (Campus First / Program
2096// First / None), configured under DataFlow Sync > General > Campus-Program
2097// Chaining and localized as ninjaFormDataJS.formCampusSettings, keyed to
2098// ninjaFormDataJS.ninjaFormsIDs by form ID.
2099function getFormDataSyncSetting(form) {
2100    const i = ninjaFormDataJS.ninjaFormsIDs.indexOf(form.id);
2101    return i > -1 ? ninjaFormDataJS.formCampusSettings[i] : undefined;
2102}
2103
2104function useDynamicCampusProgram(form) {
2105    const dataSyncSetting = getFormDataSyncSetting(form); // 'campus' | 'program' | 'no' | undefined
2106
2107    // Page-level override: the "Form is Program First?" checkbox in the
2108    // Page Editor (TCS Field Config metabox) FORCIBLY wins over the Data
2109    // Sync setting whenever it's checked -- e.g. Data Sync says
2110    // "Campus First" (or even "None") but this page has the checkbox
2111    // checked -> use Program First regardless.
2112    const pageConfig = ninjaFormDataJS.pageFieldConfig;
2113    if (pageConfig && pageConfig.program_first) {
2114        return 'program';
2115    }
2116
2117    // Data Sync explicitly says this form uses no campus/program chaining at
2118    // all, and the checkbox above didn't override it -- no chaining.
2119    if (dataSyncSetting === 'no') {
2120        return false;
2121    }
2122
2123    // No page-level override -- default to whatever Data Sync says for this form.
2124    if (dataSyncSetting === 'campus') return 'campus';
2125    if (dataSyncSetting === 'program') return 'program';
2126
2127    // No Data Sync entry configured for this form -- fall back to the
2128    // Program-first progressive-filtering chain -- Program shows every
2129    // option, Campus and Semester stay empty until a Program is chosen.
2130    return 'program';
2131}
2132
2133//custom NinjaForm features
2134function captureURL(pageUrlId) {
2135    jQuery('#nf-field-' + pageUrlId).val(window.location.href).trigger('change');
2136}
2137
2138function setThankYouPage(thankYouPageId) {
2139    if (ninjaFormDataJS.thankYouPage !== 'no') {
2140        jQuery('#nf-field-' + thankYouPageId).val(ninjaFormDataJS.thankYouPage).trigger('change');
2141    }
2142}
2143
2144function getLatLong(localLatitudeId, localLongitudeId, submitButtonId) {
2145    jQuery(`#nf-field-${submitButtonId}`).prop('disabled', true);
2146    jQuery.ajax({
2147        url: `https://www.googleapis.com/geolocation/v1/geolocate?key=${ninjaFormDataJS.googleAPIKey}`,
2148        cache: false,
2149        dataType: "json",
2150        type: "POST",
2151        complete: () => {
2152            jQuery(`#nf-field-${submitButtonId}`).prop('disabled', false);
2153        },
2154        success: result => {
2155            const lat = result.location.lat;
2156            const lng = result.location.lng;
2157            jQuery(`#nf-field-${localLatitudeId}`).val(lat).trigger('change');
2158            jQuery(`#nf-field-${localLongitudeId}`).val(lng).trigger('change');
2159        },
2160        error: err => { console.log(err); }
2161    });
2162}
2163
2164function gdprRequirements(countryCodeID) {
2165    jQuery.ajax({
2166        url: `https://www.googleapis.com/geolocation/v1/geolocate?key=${ninjaFormDataJS.googleAPIKey}`,
2167        cache: false,
2168        dataType: 'json',
2169        type: 'POST',
2170        success: result => {
2171            const lat = result.location.lat;
2172            const lng = result.location.lng;
2173            jQuery.ajax({
2174                url: `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&language=en&key=${ninjaFormDataJS.googleAPIKey}`,
2175                cache: false,
2176                dataType: 'json',
2177                type: 'GET',
2178                success: result => {
2179                    const countryCode = result.results.find(x => x.types.indexOf('country') >= 0).address_components[0].short_name.toUpperCase();
2180                    jQuery(`#nf-field-${countryCodeID}`).val(countryCode).trigger('change');
2181                },
2182                error: err => { console.log(err); }
2183            });
2184        },
2185        error: err => { console.log(err); }
2186    });
2187}
2188
2189function validateZipCode(countryFieldID, countryZipTypeID, zipFieldID, postalFieldID, stateFieldID, provFieldID, intStateFieldID, countryZipStatusID, cityFieldID, submitButtonID) {
2190    const zipCodeTimeZoneValidationController = Marionette.Object.extend({
2191        initialize: function () {
2192            const fieldsChannel = Backbone.Radio.channel('fields');
2193            this.listenTo(fieldsChannel, 'change:modelValue', this.validate);
2194        },
2195        validate: model => {
2196            if (model.get('type') !== 'zip' && model.get('type') !== 'listcountry') return;
2197            const fieldValue = model.get('value');
2198            const countriesNoPostal = [{
2199                "name": "Angola",
2200                "code": "AO"
2201            }, {
2202                "name": "Antigua and Barbuda",
2203                "code": "AG"
2204            }, {
2205                "name": "Aruba",
2206                "code": "AW"
2207            }, {
2208                "name": "Bahamas",
2209                "code": "BS"
2210            }, {
2211                "name": "Belize",
2212                "code": "BZ"
2213            }, {
2214                "name": "Benin",
2215                "code": "BJ"
2216            }, {
2217                "name": "Botswana",
2218                "code": "BW"
2219            }, {
2220                "name": "Burkina Faso",
2221                "code": "BF"
2222            }, {
2223                "name": "Burundi",
2224                "code": "BI"
2225            }, {
2226                "name": "Cameroon",
2227                "code": "CM"
2228            }, {
2229                "name": "Central African Republic",
2230                "code": "CF"
2231            }, {
2232                "name": "Comoros",
2233                "code": "KM"
2234            }, {
2235                "name": "Congo",
2236                "code": "CG"
2237            }, {
2238                "name": "Congo, the Democratic Republic of the",
2239                "code": "CD"
2240            }, {
2241                "name": "Cook Islands",
2242                "code": "CK"
2243            }, {
2244                "name": "Cote d'Ivoire",
2245                "code": "CI"
2246            }, {
2247                "name": "Djibouti",
2248                "code": "DJ"
2249            }, {
2250                "name": "Dominica",
2251                "code": "DM"
2252            }, {
2253                "name": "Equatorial Guinea",
2254                "code": "GQ"
2255            }, {
2256                "name": "Eritrea",
2257                "code": "ER"
2258            }, {
2259                "name": "Fiji",
2260                "code": "FJ"
2261            }, {
2262                "name": "French Southern Territories",
2263                "code": "TF"
2264            }, {
2265                "name": "Gambia",
2266                "code": "GM"
2267            }, {
2268                "name": "Ghana",
2269                "code": "GH"
2270            }, {
2271                "name": "Grenada",
2272                "code": "GD"
2273            }, {
2274                "name": "Guinea",
2275                "code": "GN"
2276            }, {
2277                "name": "Guyana",
2278                "code": "GY"
2279            }, {
2280                "name": "Hong Kong",
2281                "code": "HK"
2282            }, {
2283                "name": "Ireland",
2284                "code": "IE"
2285            }, {
2286                "name": "Israel",
2287                "code": "IL"
2288            }, {
2289                "name": "Jamaica",
2290                "code": "JM"
2291            }, {
2292                "name": "Kenya",
2293                "code": "KE"
2294            }, {
2295                "name": "Kiribati",
2296                "code": "KI"
2297            }, {
2298                "name": "Macao",
2299                "code": "MO"
2300            }, {
2301                "name": "Malawi",
2302                "code": "MW"
2303            }, {
2304                "name": "Mali",
2305                "code": "ML"
2306            }, {
2307                "name": "Mauritania",
2308                "code": "MR"
2309            }, {
2310                "name": "Mauritius",
2311                "code": "MU"
2312            }, {
2313                "name": "Montserrat",
2314                "code": "MS"
2315            }, {
2316                "name": "Nauru",
2317                "code": "NR"
2318            }, {
2319                "name": "Netherlands Antilles",
2320                "code": "AN"
2321            }, {
2322                "name": "Nigeria",
2323                "code": "NG"
2324            }, {
2325                "name": "Niue",
2326                "code": "NU"
2327            }, {
2328                "name": "North Korea",
2329                "code": "KP"
2330            }, {
2331                "name": "Panama",
2332                "code": "PA"
2333            }, {
2334                "name": "Qatar",
2335                "code": "QA"
2336            }, {
2337                "name": "Rwanda",
2338                "code": "RW"
2339            }, {
2340                "name": "Saint Kitts and Nevis",
2341                "code": "KN"
2342            }, {
2343                "name": "Saint Lucia",
2344                "code": "LC"
2345            }, {
2346                "name": "Sao Tome and Principe",
2347                "code": "ST"
2348            }, {
2349                "name": "Saudi Arabia",
2350                "code": "SA"
2351            }, {
2352                "name": "Seychelles",
2353                "code": "SC"
2354            }, {
2355                "name": "Sierra Leone",
2356                "code": "SL"
2357            }, {
2358                "name": "Solomon Islands",
2359                "code": "SB"
2360            }, {
2361                "name": "Somalia",
2362                "code": "SO"
2363            }, {
2364                "name": "South Africa",
2365                "code": "ZA"
2366            }, {
2367                "name": "Suriname",
2368                "code": "SR"
2369            }, {
2370                "name": "Syria",
2371                "code": "SY"
2372            }, {
2373                "name": "Tanzania, United Republic of",
2374                "code": "TZ"
2375            }, {
2376                "name": "Timor-Leste",
2377                "code": "TL"
2378            }, {
2379                "name": "Tokelau",
2380                "code": "TK"
2381            }, {
2382                "name": "Tonga",
2383                "code": "TO"
2384            }, {
2385                "name": "Trinidad and Tobago",
2386                "code": "TT"
2387            }, {
2388                "name": "Tuvalu",
2389                "code": "TV"
2390            }, {
2391                "name": "Uganda",
2392                "code": "UG"
2393            }, {
2394                "name": "United Arab Emirates",
2395                "code": "AE"
2396            }, {
2397                "name": "Vanuatu",
2398                "code": "VU"
2399            }, {
2400                "name": "Yemen",
2401                "code": "YE"
2402            }, {
2403                "name": "Zimbabwe",
2404                "code": "ZW"
2405            }];
2406            if (model.get('type') === 'listcountry') {
2407                jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2408                jQuery('#nf-field-' + postalFieldID).val("");
2409                jQuery('#nf-field-' + zipFieldID).val("");
2410                jQuery('#nf-field-' + intStateFieldID).val("").trigger('change');
2411                jQuery('#nf-field-' + cityFieldID).val("");
2412                Backbone.Radio.channel('fields').request('remove:error', postalFieldID, 'required-error');
2413                if (fieldValue === "US") {
2414                    jQuery('#nf-field-' + countryZipTypeID).val("us").trigger('change');
2415                } else if (fieldValue === "CA") {
2416                    jQuery('#nf-field-' + countryZipTypeID).val("ca").trigger('change');
2417                } else {
2418                    const countryPostalResult = jQuery.map(countriesNoPostal, val => {
2419                        return val.code === fieldValue ? 'nopost' : null;
2420                    });
2421                    if (countryPostalResult[0] === 'nopost') {
2422                        jQuery('#nf-field-' + countryZipTypeID).val("nopost").trigger('change');
2423                        jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2424                        jQuery("label[for=nf-field-" + postalFieldID + "]").html('Postal Code');
2425                        jQuery('#nf-field-' + cityFieldID).val("");
2426                        jQuery('#nf-field-' + intStateFieldID).val("").trigger('change');
2427                        Backbone.Radio.channel('fields').request('remove:error', postalFieldID, 'required-error');
2428                        Backbone.Radio.channel('fields').request('remove:error', zipFieldID, 'required-error');
2429                    } else {
2430                        jQuery('#nf-field-' + countryZipTypeID).val("int").trigger('change');
2431                        jQuery("label[for=nf-field-" + postalFieldID + "]").html('Postal Code <span class="ninja-forms-req-symbol">*</span>');
2432                    }
2433                }
2434            }
2435            if (model.get('type') === 'zip') {
2436                if (!model.get('value')) return;
2437                console.log('ZIP VALIDATION START - zipValue:', model.get('value'), 'countryZipType:', jQuery('#nf-field-' + countryZipTypeID).val());
2438                if (jQuery('#nf-field-' + countryZipTypeID).val() === 'nopost') {
2439                    jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2440                } else {
2441                    jQuery('#nf-field-' + zipFieldID).parent().append('<img src="' + ninjaFormDataJS.loadingGifPath + '" alt="tcspp-loader" class="zip-loader" width="118" height="15" />');
2442                    jQuery('#nf-field-' + zipFieldID).prop('disabled', true);
2443                    jQuery('#nf-field-' + submitButtonID).prop('disabled', true);
2444                    const country = jQuery(`#nf-field-${countryFieldID}`).find(':selected').text();
2445                    const apiKey = ninjaFormDataJS.googleAPIKey || 'AIzaSyBadUrU21eiyl9PJ0I7VovULzAPwlL7FUo';
2446                    const searchAddress = model.get('value') + ' ' + country;
2447                    const apiUrl = "https://maps.googleapis.com/ma
2447ps/api/geocode/json?address=" + encodeURIComponent(searchAddress) + '&language=en' + "&key=" + apiKey;
2448                    
2449                    console.log('ZIP API CALL:', {
2450                        zipValue: model.get('value'),
2451                        country: country,
2452                        searchAddress: searchAddress,
2453                        hasAPIKey: !!apiKey,
2454                        apiKeyLength: apiKey ? apiKey.length : 0,
2455                        url: apiUrl.substring(0, 100) + '...'
2456                    });
2457                    
2458                    jQuery.ajax({
2459                        url: apiUrl,
2460                        cache: false,
2461                        dataType: "json",
2462                        type: "GET",
2463                        complete: () => {
2464                            console.log('ZIP AJAX COMPLETE');
2465                            jQuery('.zip-loader').remove();
2466                            jQuery('#nf-field-' + zipFieldID).prop('disabled', false);
2467                            jQuery('#nf-field-' + submitButtonID).prop('disabled', false);
2468                        },
2469                        success: result => {
2470                            console.log('ZIP AJAX SUCCESS - results length:', result.results.length);
2471                            console.log('ZIP AJAX FULL RESPONSE:', result);
2472                            
2473                            // Handle API key restriction errors - but validate format first
2474                            if (result.status === 'REQUEST_DENIED') {
2475                                console.log('ZIP - API Request Denied (likely referer restriction). Validating format...');
2476                                
2477                                // Basic format validation for common zip codes
2478                                const zipValue = model.get('value');
2479                                const countryZipType = jQuery('#nf-field-' + countryZipTypeID).val();
2480                                let isValidFormat = false;
2481                                
2482                                if (countryZipType === 'us') {
2483                                    // US: 5 digits (12345) or 5+4 (12345-6789)
2484                                    isValidFormat = /^\d{5}(-\d{4})?$/.test(zipValue);
2485                                } else if (countryZipType === 'ca') {
2486                                    // Canada: A1A 1A1 format (letter-digit-letter space digit-letter-digit)
2487                                    isValidFormat = /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i.test(zipValue);
2488                                } else if (countryZipType === 'int') {
2489                                    // International: at least 3 characters, alphanumeric
2490                                    isValidFormat = /^[A-Z0-9\s\-]{3,}$/i.test(zipValue);
2491                                }
2492                                
2493                                if (isValidFormat) {
2494                                    console.log('ZIP - Format valid. Accepting zip code despite API restriction.');
2495                                    Backbone.Radio.channel('fields').request('remove:error', model.get('id'), 'zip-code-error');
2496                                    jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2497                                } else {
2498                                    console.log('ZIP - Format invalid for type:', countryZipType, 'value:', zipValue);
2499                                    Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2500                                    jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2501                                }
2502                                return;
2503                            }
2504                            
2505                            if (typeof jQuery('#nf-field-' + countryFieldID).val() === 'undefined') return;
2506                            if (result.results.length > 0) {
2507                                const address = result.results.find(r => typeof r.address_components.find(ac => typeof ac.types.find(t => t === 'postal_code') !== 'undefined') !== 'undefined');
2508                                
2509                                console.log('ZIP - Found address with postal code:', typeof address !== 'undefined');
2510                                if (typeof address !== 'undefined') {
2511                                    console.log('ZIP - Address components:', address.address_components.map(c => ({types: c.types, short_name: c.short_name})));
2512                                    
2513                                    if (address.address_components.find(ac => typeof ac.types.find(t => t === 'country') !== 'undefined').short_name === jQuery('#nf-field-' + countryFieldID).val()) {
2514                                        Backbone.Radio.channel('fields').request('remove:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2515                                        jQuery('#nf-field-' + countryZipStatusID).val("filled").trigger('change');
2516                                        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') {
2517                                            // Find components by type, not by index
2518                                            const localityComponent = address.address_components.find(ac => ac.types.includes('locality'));
2519                                            const stateComponent = address.address_components.find(ac => ac.types.includes('administrative_area_level_1'));
2520                                            
2521                                            console.log('ZIP - Found city:', localityComponent?.short_name, 'state:', stateComponent?.short_name);
2522                                            
2523                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'us') {
2524                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2525                                                if (stateComponent) jQuery('#nf-field-' + stateFieldID).val(stateComponent.short_name).trigger('change');
2526                                            }
2527                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'ca') {
2528                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2529                                                if (stateComponent) jQuery('#nf-field-' + provFieldID).val(stateComponent.short_name).trigger('change');
2530                                            }
2531                                        } else {
2532                                            if (jQuery('#nf-field-' + countryZipTypeID).val() === 'int') {
2533                                                const localityComponent = address.address_components.find(ac => ac.types.includes('locality'));
2534                                                if (localityComponent) jQuery('#nf-field-' + cityFieldID).val(localityComponent.short_name).trigger('change');
2535                                            }
2536                                        }
2537                                    } else {
2538                                        console.log('ZIP - Country mismatch');
2539                                        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.');
2540                                        jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2541                                    }
2542                                }
2543                                else {
2544                                    console.log('ZIP - No address with postal code found');
2545                                    Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2546                                }
2547                            }
2548                            else {
2549                                console.log('ZIP - No results returned');
2550                                Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2551                            }
2552                        },
2553                        error: (xhr, status, error) => {
2554                            console.log('ZIP AJAX ERROR:', {status, error, responseText: xhr.responseText});
2555                            // Only accept on REQUEST_DENIED (API restriction), reject on other errors
2556                            Backbone.Radio.channel('fields').request('add:error', model.get('id'), 'zip-code-error', 'Please Enter A Valid Zip Code');
2557                            jQuery('#nf-field-' + countryZipStatusID).val("ready").trigger('change');
2558                        }
2559                    });
2560                }
2561            }
2562        }
2563    });
2564    new zipCodeTimeZoneValidationController();
2565}
2566
2567function setEmailValidation() {
2568    const emailValidator = Marionette.Object.extend({
2569        initialize: function () {
2570            const submitChannel = Backbone.Radio.channel('submit');
2571            this.listenTo(submitChannel, 'validate:field', this.validate);
2572            const fieldsChannel = Backbone.Radio.channel('fields');
2573            this.listenTo(fieldsChannel, 'change:modelValue', this.validate);
2574        },
2575        validate: function (model) {
2576            if (model.get('type') !== 'email') return;
2577            const fieldValue = model.get('value');
2578            const modelId = model.get('id');
2579            const errorId = 'custom-email-error';
2580            const errorMessage = 'Please enter a valid email address!';
2581            const fieldsChannel = Backbone.Radio.channel('fields');
2582            if (/^[A-z|0-9|.]+@[A-z|0-9|.]+.[A-z]+$/.test(fieldValue)) {
2583                fieldsChannel.request('remove:error', modelId, errorId);
2584            } else {
2585                fieldsChannel.request('add:error', modelId, errorId, errorMessage);
2586            }
2587        }
2588    });
2589    new emailValidator();
2590}
2591
2592function limitDates(dateLimit) {
2593    const dateLimiter = Marionette.Object.extend({
2594        initialize: function () {
2595            const pikadayChannel = Backbone.Radio.channel('pikaday');
2596            this.listenTo(pikadayChannel, 'init', this.modifyDatepicker);
2597        },
2598        modifyDatepicker: (dateObject, fieldModel) => {
2599            let currentDate = new Date();
2600            const tomorrow = currentDate.setDate(currentDate.getDate() + 1);
2601            currentDate = new Date();
2602            const futureweek = currentDate.setDate(currentDate.getDate() + dateLimit);
2603            dateObject.pikaday._o.disableWeekends = true;
2604            dateObject.pikaday._o.firstDay = 0;
2605            dateObject.pikaday._o.maxDate = futureweek;
2606            dateObject.pikaday._o.minDate = tomorrow;
2607            dateObject.pikaday._o.disableDayFn = (date) => {
2608                const disableDays = ninjaFormDataJS.holidayBlackOuts;
2609                if (_.indexOf(disableDays, moment(date).format("YYYY-MM-DD")) !== -1) {
2610                    return true;
2611                }
2612            }
2613        }
2614    });
2615    new dateLimiter();
2616}
2617
2618function getLeadSourceID(srcParamTranslatedID) {
2619    const leadSourceID = ninjaFormDataJS.leadSourceID;
2620
2621    jQuery('#nf-field-' + srcParamTranslatedID)
2622        .val(leadSourceID)
2623        .trigger('change');
2624
2625    // Safely push to dataLayer only if it exists
2626    if (typeof window.dataLayer !== 'undefined' && Array.isArray(window.dataLayer)) {
2627        window.dataLayer.push({
2628            'tcsLeadSourceID': leadSourceID
2629        });
2630    } else {
2631        console.warn('dataLayer not found: skipping GTM push');
2632    }
2633}
2634
2635function programCampusDropDowns(programID, programLabelID, campusID, campusLabelID) {
2636    // Remove any empty data-solo-campus-id options
2637    jQuery(`#nf-field-${programID} option[data-solo-campus-id=""]`).remove();
2638
2639    jQuery("#nf-field-" + programID).change(() => {
2640        const programValue = jQuery("#nf-field-" + programID).val();
2641
2642        // Reset campus dropdown with default option
2643        initializeDropdownWithDefault(campusID, 'Choose Campus');
2644
2645        if (programValue) {
2646            jQuery("#nf-field-" + campusID).append(ninjaFormDataJS.campusesHTML);
2647
2648            // Try to filter using mapping, fallback to data-class if no mapping
2649            filterCampusesByProgramMapping(programValue, campusID, programID);
2650
2651            jQuery("#nf-field-" + campusID + " optgroup").each((index, option) => {
2652                if (jQuery(option).is(':empty')) {
2653                    jQuery(option).remove();
2654                }
2655            });
2656        }
2657
2658        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2659        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2660    });
2661
2662    // Campus change handler for label updates
2663    jQuery("#nf-field-" + campusID).change(() => {
2664        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2665        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2666    });
2667}
2668
2669// Start for semester based on campus
2670function programSemesterDropDowns(campusID, campusLabelID, semesterID, semesterLabelID, programID, programLabelID, selectedFirst) {
2671
2672    jQuery("#nf-field-" + semesterID + " option").remove();
2673    jQuery("#nf-field-" + semesterID + " optgroup").remove();
2674    jQuery("#nf-field-" + semesterID).append('<option value="" label="">--</option>');
2675
2676    let selectedProgramSemesterHTML = '';
2677    let selectedCampusSemesterHTML = '';
2678    let semester_selected_ProgValue = '';
2679
2680    if (selectedFirst == 'program') {
2681        jQuery("#nf-field-" + programID).change(() => {
2682            const semesterProgValue = jQuery("#nf-field-" + programID).val();
2683            semester_selected_ProgValue = semesterProgValue;
2684            jQuery("#nf-field-" + semesterID + " option").remove();
2685            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2686            jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
2687            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
2688
2689            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2690                let optionSemestersProgid = jQuery(option).attr('data-program-solo-id');
2691
2692                if (typeof optionSemestersProgid !== 'undefined' && semesterProgValue !== '') {
2693                    optionSemestersProgid = optionSemestersProgid.split(' ');
2694                    var Semester_len = optionSemestersProgid.length;
2695
2696                    if (optionSemestersProgid.indexOf(semesterProgValue) === 0) {
2697                        //option.setAttribute('selected','selected');
2698                    }
2699                    if (optionSemestersProgid.indexOf(semesterProgValue) === -1 && option.value !== '') {
2700                        jQuery(option).remove();
2701                    }
2702                }
2703            });
2704
2705            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2706                if (jQuery(option).is(':empty')) {
2707                    jQuery(option).remove();
2708                }
2709            });
2710
2711            selectedProgramSemesterHTML = jQuery("#nf-field-" + semesterID).html();
2712
2713        });
2714
2715        jQuery("#nf-field-" + campusID).change(() => {
2716            const semesterValue = jQuery("#nf-field-" + campusID).val();
2717            jQuery("#nf-field-" + semesterID + " option").remove();
2718            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2719            jQuery("#nf-field-" + semesterID).append(selectedProgramSemesterHTML);
2720
2721            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2722                let option_value_Semesters = jQuery(option).attr('data-class');
2723                let optionSemesters = '';
2724
2725                if (typeof option_value_Semesters !== 'undefined' && semesterValue !== '') {
2726                    optionSemesters = option_value_Semesters
2727                        .split(" ") // Split the string into an array
2728                        .filter(pair => pair.startsWith(semester_selected_ProgValue + "-")) // Filter pairs matching the selected ID
2729                        .map(pair => pair.split("-")[1]) // Extract only the values
2730                        .join(" ");
2731                }
2732
2733                if (typeof optionSemesters !== 'undefined' && semesterValue !== '') {
2734                    optionSemesters = optionSemesters.split(' ');
2735                    var Semester_len = optionSemesters.length;
2736
2737                    if (optionSemesters.indexOf(semesterValue) === 0) {
2738                        //option.setAttribute('selected','selected');
2739                    }
2740                    if (optionSemesters.indexOf(semesterValue) === -1 && option.value !== '') {
2741                        jQuery(option).remove();
2742                    }
2743                }
2744            });
2745
2746            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2747                if (jQuery(option).is(':empty')) {
2748                    jQuery(option).remove();
2749                }
2750            });
2751
2752            const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2753            jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2754
2755            singleSemesterSelect(semesterID, campusID);
2756        });
2757        jQuery("#nf-field-" + semesterID).change(() => {
2758            const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2759            jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2760        });
2761
2762    } else if (selectedFirst == 'campus') {
2763
2764        jQuery("#nf-field-" + campusID).change(() => {
2765            const semesterValue = jQuery("#nf-field-" + campusID).val();
2766            jQuery("#nf-field-" + semesterID + " option").remove();
2767            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2768            jQuery("#nf-field-" + semesterID).append('<option value="" label="">Choose Semester</option>');
2769            jQuery("#nf-field-" + semesterID).append(ninjaFormDataJS.semestersHTML);
2770
2771            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2772                let optionSemesters = jQuery(option).attr('data-class');
2773
2774                if (typeof optionSemesters !== 'undefined' && semesterValue !== '') {
2775                    optionSemesters = optionSemesters.split(' ');
2776                    var Semester_len = optionSemesters.length;
2777
2778                    if (optionSemesters.indexOf(semesterValue) === 0) {
2779                        //option.setAttribute('selected','selected');
2780                    }
2781                    if (optionSemesters.indexOf(semesterValue) === -1 && option.value !== '') {
2782                        jQuery(option).remove();
2783                    }
2784                }
2785            });
2786
2787            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2788                if (jQuery(option).is(':empty')) {
2789                    jQuery(option).remove();
2790                }
2791            });
2792
2793            selectedCampusSemesterHTML = jQuery("#nf-field-" + semesterID).html();
2794
2795            const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2796            jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2797
2798            singleSemesterSelect(semesterID, campusID);
2799        });
2800
2801        jQuery("#nf-field-" + programID).change(() => {
2802            const semesterProgValue = jQuery("#nf-field-" + programID).val();
2803            jQuery("#nf-field-" + semesterID + " option").remove();
2804            jQuery("#nf-field-" + semesterID + " optgroup").remove();
2805            jQuery("#nf-field-" + semesterID).append(selectedCampusSemesterHTML);
2806
2807            jQuery("#nf-field-" + semesterID + " option").each((index, option) => {
2808                let optionSemestersProgid = jQuery(option).attr('data-program-solo-id');
2809
2810                if (typeof optionSemestersProgid !== 'undefined' && semesterProgValue !== '') {
2811                    optionSemestersProgid = optionSemestersProgid.split(' ');
2812                    var Semester_len = optionSemestersProgid.length;
2813
2814                    if (optionSemestersProgid.indexOf(semesterProgValue) === 0) {
2815                        //option.setAttribute('selected','selected');
2816                    }
2817                    if (optionSemestersProgid.indexOf(semesterProgValue) === -1 && option.value !== '') {
2818                        jQuery(option).remove();
2819                    }
2820                }
2821            });
2822
2823            jQuery("#nf-field-" + semesterID + " optgroup").each((index, option) => {
2824                if (jQuery(option).is(':empty')) {
2825                    jQuery(option).remove();
2826                }
2827            });
2828        });
2829
2830        jQuery("#nf-field-" + semesterID).change(() => {
2831            const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2832            jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2833        });
2834
2835    }
2836
2837}
2838
2839function singleSemesterSelect(semesterID, campusID) {
2840    var $select = jQuery("#nf-field-" + semesterID);
2841    var optionsWithValue = $select.find("option").filter(function () {
2842        return jQuery(this).val().trim() !== "";
2843    });
2844
2845    var optionCountWithValue = optionsWithValue.length;
2846
2847    if (optionCountWithValue === 1) {
2848        optionsWithValue.prop("selected", true);
2849        jQuery("#nf-field-" + semesterID).trigger('change');
2850    }
2851}
2852
2853// End for semester
2854
2855function nonChainedDropDowns(programID, programLabelID, campusID, campusLabelID, campusDataField) {
2856    // Initialize both dropdowns with default options
2857    initializeDropdownWithDefault(programID, '--');
2858    initializeDropdownWithDefault(campusID, '--');
2859
2860    jQuery("#nf-field-" + campusID).change(function () {
2861        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2862        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2863    });
2864
2865    jQuery("#nf-field-" + programID).change(function () {
2866        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2867        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2868
2869        if (typeof campusDataField !== 'undefined') {
2870            const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2871            jQuery("#nf-field-" + campusDataField.id).val(dataSoloCampusID).trigger('change');
2872            if (typeof dataSoloCampusID !== 'undefined') {
2873                const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2874                    return ninjaFormDataJS.campusData[key];
2875                });
2876                const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2877                if (typeof soloCampus !== 'undefined') {
2878                    jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2879                }
2880            }
2881        }
2882    });
2883}
2884
2885// Non-chained dropdown behavior for forms without school (useDynamic === '')
2886function nonChainedDropDownsNoSchool(programID, programLabelID, campusID, campusLabelID, semesterID, semesterLabelID, campusDataField, countryFieldId, stateFieldId) {
2887
2888    // Initialize all dropdowns with their full options including defaults
2889    // Initialize program field with all options
2890    const $programSelect = jQuery('#nf-field-' + programID);
2891    $programSelect.find('option').remove();
2892    $programSelect.find('optgroup').remove();
2893    $programSelect.append('<option value="" label="">Choose Program</option>');
2894    $programSelect.append(ninjaFormDataJS.programsHTML);
2895
2896    // Initialize campus field with all options  
2897    const $campusSelect = jQuery('#nf-field-' + campusID);
2898    $campusSelect.find('option').remove();
2899    $campusSelect.find('optgroup').remove();
2900    $campusSelect.append('<option value="" label="">Choose Campus</option>');
2901    $campusSelect.append(ninjaFormDataJS.campusesHTML);
2902
2903    // Initialize semester field with all options
2904    const $semesterSelect = jQuery('#nf-field-' + semesterID);
2905    $semesterSelect.find('option').remove();
2906    $semesterSelect.find('optgroup').remove();
2907    $semesterSelect.append('<option value="" label="">Choose Semester</option>');
2908    $semesterSelect.append(ninjaFormDataJS.semestersHTML);
2909
2910    // Set up change handlers for labels only (no filtering)
2911    jQuery("#nf-field-" + programID).change(function () {
2912        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
2913        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
2914
2915        // Handle campus data field if it exists
2916        if (typeof campusDataField !== 'undefined' && campusDataField) {
2917            const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2918            jQuery("#nf-field-" + campusDataField.id).val(dataSoloCampusID).trigger('change');
2919            if (typeof dataSoloCampusID !== 'undefined') {
2920                const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2921                    return ninjaFormDataJS.campusData[key];
2922                });
2923                const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2924                if (typeof soloCampus !== 'undefined') {
2925                    jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2926                }
2927            }
2928        }
2929        runRuleEngine();
2930    });
2931
2932    jQuery("#nf-field-" + campusID).change(function () {
2933        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
2934        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
2935        runRuleEngine();
2936    });
2937
2938    jQuery("#nf-field-" + semesterID).change(function () {
2939        const semesterLabel = jQuery("#nf-field-" + semesterID + " option:selected").text();
2940        jQuery("#nf-field-" + semesterLabelID).val(semesterLabel).trigger('change');
2941        runRuleEngine();
2942    });
2943
2944    jQuery("#nf-field-" + countryFieldId?.id).change(function () {
2945        runRuleEngine();
2946    })
2947    jQuery("#nf-field-" + stateFieldId?.id).change(function () {
2948        runRuleEngine();
2949    })
2950
2951    function runRuleEngine() {
2952
2953        const values = {
2954            program: jQuery("#nf-field-" + programID).val(),
2955            campus: jQuery("#nf-field-" + campusID).val(),
2956            semester: jQuery("#nf-field-" + semesterID).val(),
2957            country: jQuery("#nf-field-" + countryFieldId?.id)?.val(),
2958            state: jQuery("#nf-field-" + stateFieldId?.id)?.val(),
2959        };
2960
2961        const fieldIDs = {
2962            program: programID,
2963            campus: campusID,
2964            semester: semesterID,
2965            country: countryFieldId?.id,
2966            state: stateFieldId?.id
2967        };
2968
2969        // STEP 1: RESET everything first
2970        resetRuleEffects(fieldIDs);
2971
2972        // STEP 2: Apply rules fresh
2973        processRules(ninjaFormDataJS.rulesData, values, fieldIDs);
2974    }
2975
2976    // Since Program, Campus, and Semester are independent (non-chained) here,
2977    // auto-select each one on load if it only has a single available option.
2978    autoSelectSingleOption(programID);
2979    autoSelectSingleOption(campusID);
2980    autoSelectSingleOption(semesterID);
2981
2982    runRuleEngine(); // run once on load
2983}
2984
2985function selectProgramCampusUTM(programID, programLabelID, campusID, campusLabelID, campusTarget, utmCampus, utmProgram, dynamicFormType) {
2986    if (dynamicFormType === 'program' && typeof utmProgram !== 'undefined' && utmProgram.value) {
2987        selectProgramUTM(programID, programLabelID, utmProgram);
2988    }
2989    if (typeof campusTarget !== 'undefined') {
2990        const dataSoloCampusID = jQuery("#nf-field-" + programID + " option:selected").data('solo-campus-id');
2991        jQuery("#nf-field-" + campusTarget.id).val(dataSoloCampusID).trigger('change');
2992        if (typeof dataSoloCampusID !== 'undefined') {
2993            const soloCampuses = Object.keys(ninjaFormDataJS.campusData).map(function (key) {
2994                return ninjaFormDataJS.campusData[key];
2995            });
2996            const soloCampus = soloCampuses.find(o => o.campus_value == dataSoloCampusID);
2997            if (typeof soloCampus !== 'undefined') {
2998                jQuery("#nf-field-" + campusLabelID).val(soloCampus.campus_label).trigger('change');
2999            }
3000        }
3001    }
3002    else if (utmCampus.value) {
3003        jQuery('#nf-field-' + campusID).val(utmCampus.value).trigger('change');
3004        const campusLabel = jQuery("#nf-field-" + campusID + " option:selected").text();
3005        jQuery("#nf-field-" + campusLabelID).val(campusLabel).trigger('change');
3006    }
3007    if (dynamicFormType !== 'program' && typeof utmProgram !== 'undefined' && utmProgram.value) {
3008        selectProgramUTM(programID, programLabelID, utmProgram);
3009    }
3010}
3011
3012function selectProgramUTM(programID, programLabelID, utmProgram) {
3013    if (utmProgram.value) {
3014        jQuery('#nf-field-' + programID).val(utmProgram.value).trigger('change');
3015        const programLabel = jQuery("#nf-field-" + programID + " option:selected").text();
3016        jQuery("#nf-field-" + programLabelID).val(programLabel).trigger('change');
3017    }
3018}
3019
3020function bindChangesPartChange(timezonefillID, submitButtonID) {
3021    const bindOnChangesOnPartChange = Marionette.Object.extend({
3022        initialize: function () {
3023            const fieldsChannel = Backbone.Radio.channel('fields');
3024            this.listenTo(fieldsChannel, 'change:model', this.fieldModelChange);
3025        },
3026        fieldModelChange: (fieldModel, formModel, conditionModel) => {
3027            if (fieldModel.changed.visible !== true) return;
3028            if ('time_zone_drop_down' == fieldModel.get('key')) {
3029                forumlateTimeZones(fieldModel.get('id'), fieldModel.get('value'), timezonefillID, submitButtonID);
3030            }
3031        }
3032    });
3033    new bindOnChangesOnPartChange();
3034}
3035
3036function forumlateTimeZones(modelID, modelValue, timezonefillID, submitButtonID) {
3037    jQuery("#nf-field-" + modelID).change(function () {
3038        if (modelValue) {
3039            const fieldValue = jQuery("#nf-field-" + modelID).val();
3040            jQuery("#nf-field-" + modelID).parent().append('<img src="' + ninjaFormDataJS.loadingGifPath + '" alt="tcspp-loader" class="timezone-loader" width="118" height="15" />');
3041            jQuery("#nf-field-" + modelID).prop('disabled', true);
3042            jQuery('#nf-field-' + submitButtonID).prop('disabled', true);
3043            jQuery.ajax({
3044                url: "https://maps.googleapis.com/maps/api/geocode/json?address=" + fieldValue + '&language=en' + "&key=" + ninjaFormDataJS.googleAPIKey,
3045                cache: false,
3046                dataType: "json",
3047                type: "GET",
3048                complete: function () {
3049                    jQuery(".timezone-loader").remove();
3050                    jQuery("#nf-field-" + modelID).prop('disabled', false);
3051                    jQuery('#nf-field-' + submitButtonID).prop('disabled', false);
3052                },
3053                success: function (result, success) {
3054                    if (success == 'success') {
3055                        const lat = result.results[0].geometry.location.lat;
3056                        const lng = result.results[0].geometry.location.lng;
3057                        const timestamp = Math.floor((new Date).getTime() / 1000);
3058                        jQuery.ajax({
3059                            url: "https://maps.googleapis.com/maps/api/timezone/json?location=" + lat + "," + lng + "&timestamp=" + timestamp + '&language=en' + "&key=" + ninjaFormDataJS.googleAPIKey,
3060                            cache: false,
3061                            dataType: "json",
3062                            type: "GET",
3063                            success: function (result, success) {
3064                                const dst = Number(result.dstOffset);
3065                                const raw = Number(result.rawOffset);
3066                                const adjustedTime = dst + raw;
3067                                const isNegative = Math.sign(adjustedTime);
3068                                const baseNums = Math.abs(adjustedTime);
3069                                if (baseNums > 0) {
3070                                    const timeinHours = secondsTimeSpanToHMS(baseNums);
3071                                    let appendDig = "";
3072                                    if (isNegative < 0) appendDig = '-';
3073                                    else appendDig = '+';
3074                                    const timeString = appendDig + timeinHours;
3075                                    jQuery("#nf-field-" + timezonefillID).val(timeString).trigger('change');
3076                                }
3077                            },
3078                            error: function (result, success) { }
3079                        });
3080                    }
3081                }
3082            });
3083        }
3084    });
3085}
3086
3087function secondsTimeSpanToHMS(seconds) {
3088    const hours = Math.floor(seconds / 3600);
3089    seconds -= hours * 3600;
3090    const minutes = Math.floor(seconds / 60);
3091    seconds -= minutes * 60;
3092    return (hours < 10 ? '0' + hours : hours) + ":" + (minutes < 10 ? '0' + minutes : minutes); //zero padding on minutes and seconds
3093}
3094
3095function setupAddressAutocomplete(countryFieldId) {
3096    jQuery(`#nf-field-${countryFieldId}`).prop('autocomplete', 'country');
3097    if (typeof jQuery(`#nf-field-${countryFieldId}`)[0] !== 'undefined') {
3098        clearInterval(autocompleteSetupInterval);
3099    }
3100}
3101// NOTE: useDynamicCampusProgram(form) now lives in one place, alongside the
3102// 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.