PageSourceSearch

https://www.emeris.ac.za/js/enquiryform-faux.js

js emeris.ac.za collected 2026-10-09 05:00:59 UTC 67,033 bytes, 1,402 lines download raw bytes

1(function () {
2    'use strict';
3
4    // ============================================================
5    // Country dial codes — populated into phone group selects by JS
6    // ============================================================
7
8    const COUNTRY_CODES = [
9        '+1', '+20', '+211', '+212', '+213', '+216', '+218', '+220', '+221', '+222', '+223', '+224', '+225', '+226',
10        '+227', '+228', '+229', '+230', '+231', '+232', '+233', '+234', '+235', '+236', '+237', '+238', '+239',
11        '+240', '+241', '+242', '+243', '+244', '+245', '+246', '+248', '+249', '+250', '+251', '+252', '+253',
12        '+254', '+255', '+256', '+257', '+258', '+260', '+261', '+262', '+263', '+264', '+265', '+266', '+267',
13        '+268', '+269', '+27', '+290', '+291', '+297', '+298', '+299', '+30', '+31', '+32', '+33', '+34',
14        '+350', '+351', '+352', '+353', '+354', '+355', '+356', '+357', '+358', '+359', '+36', '+370', '+371',
15        '+372', '+373', '+374', '+375', '+376', '+377', '+378', '+379', '+380', '+381', '+382', '+383', '+385',
16        '+386', '+387', '+389', '+39', '+40', '+41', '+420', '+421', '+423', '+43', '+44', '+45', '+46', '+47',
17        '+48', '+49', '+500', '+501', '+502', '+503', '+504', '+505', '+506', '+507', '+508', '+509', '+51',
18        '+52', '+53', '+54', '+55', '+56', '+57', '+58', '+590', '+591', '+592', '+593', '+595', '+597', '+598',
19        '+599', '+60', '+61', '+62', '+63', '+64', '+65', '+66', '+670', '+672', '+673', '+674', '+675', '+676',
20        '+677', '+678', '+679', '+680', '+681', '+682', '+683', '+685', '+686', '+687', '+688', '+689', '+690',
21        '+691', '+692', '+7', '+81', '+82', '+84', '+850', '+852', '+853', '+855', '+856', '+86', '+880', '+886',
22        '+90', '+91', '+92', '+93', '+94', '+95', '+960', '+961', '+962', '+963', '+964', '+965', '+966', '+967',
23        '+968', '+970', '+971', '+972', '+973', '+974', '+975', '+976', '+977', '+98', '+992', '+993', '+994',
24        '+995', '+996', '+998'
25    ];
26
27    const DEFAULT_DIAL_CODE = '+27';
28
29    function populateCountryCodes(selectId) {
30        var sel = document.getElementById(selectId);
31        if (!sel) return;
32        var frag = document.createDocumentFragment();
33        COUNTRY_CODES.forEach(function (code) {
34            var opt = document.createElement('option');
35            opt.value = code;
36            opt.textContent = code;
37            if (code === DEFAULT_DIAL_CODE) opt.selected = true;
38            frag.appendChild(opt);
39        });
40        sel.appendChild(frag);
41    }
42
43    // ============================================================
44    // Country list + Province/City map (South Africa only)
45    // ============================================================
46
47    const COUNTRIES = [
48        'South Africa', 'Botswana', 'Congo, Dem. Rep. of the', 'Ghana', 'Kenya', 'Malawi', 'Mauritius',
49        'Namibia', 'Nigeria', 'Swaziland', 'Tanzania, United Rep. of', 'Uganda', 'Zambia', 'Zimbabwe',
50        'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina',
51        'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados',
52        'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Brazil',
53        'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada',
54        'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Republic)',
55        'Congo (Democratic Republic)', 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic',
56        'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador',
57        'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', 'Finland', 'France',
58        'Gabon', 'Gambia', 'Georgia', 'Germany', 'Greece', 'Grenada', 'Guatemala', 'Guinea',
59        'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia',
60        'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Jap
60an', 'Jordan', 'Kazakhstan',
61        'Kiribati', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia',
62        'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malaysia', 'Maldives', 'Mali',
63        'Malta', 'Marshall Islands', 'Mauritania', 'Mexico', 'Micronesia', 'Moldova', 'Monaco',
64        'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar', 'Nauru', 'Nepal', 'Netherlands',
65        'New Zealand', 'Nicaragua', 'Niger', 'North Korea', 'North Macedonia', 'Norway', 'Oman',
66        'Pakistan', 'Palau', 'Palestine', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines',
67        'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis',
68        'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino',
69        'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone',
70        'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Korea', 'South Sudan',
71        'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Taiwan',
72        'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago',
73        'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Ukraine', 'United Arab Emirates',
74        'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City',
75        'Venezuela', 'Vietnam', 'Yemen'
76    ];
77
78    const PROVINCE_CITY_MAP = {
79        'Gauteng': ['Johannesburg', 'Pretoria', 'Soweto', 'Sandton', 'Midrand', 'Centurion', 'Benoni', 'Alberton', 'Germiston', 'Randburg', 'Roodepoort', 'Krugersdorp', 'Vereeniging', 'Kempton Park', 'Vanderbijlpark', 'Other'],
80        'Western Cape': ['Cape Town', 'Stellenbosch', 'George', 'Paarl', 'Worcester', 'Knysna', 'Mossel Bay', 'Hermanus', 'Somerset West', 'Oudtshoorn', 'Beaufort West', 'Plettenberg Bay', 'Grabouw', 'Langebaan', 'Other'],
81        'Eastern Cape': ['Port Elizabeth (Gqeberha)', 'East London', 'Mthatha', 'Bhisho', 'Queenstown', "King William's Town", 'Grahamstown', 'Port Alfred', 'Jeffreys Bay', 'Humansdorp', 'Uitenhage', 'Other'],
82        'KwaZulu-Natal': ['Durban', 'Pietermaritzburg', 'Richards Bay', 'Newcastle', 'Ballito', 'Empangeni', 'Port Shepstone', 'Pinetown', 'Amanzimtoti', 'Tongaat', 'Estcourt', 'St. Lucia', 'Umhlanga', 'Other'],
83        'Free State': ['Bloemfontein', 'Welkom', 'Phuthaditjhaba', 'Kroonstad', 'Bethlehem', 'Sasolburg', 'Virginia', 'Odendaalsrus', 'Parys', 'Botshabelo', 'Other'],
84        'Limpopo': ['Polokwane', 'Tzaneen', 'Thohoyandou', 'Louis Trichardt', 'Bela-Bela', 'Mokopane', 'Lephalale', 'Phalaborwa', 'Groblersdal', 'Burgersfort', 'Other'],
85        'Mpumalanga': ['Mbombela (Nelspruit)', 'Witbank (eMalahleni)', 'Middelburg', 'Secunda', 'Standerton', 'Barberton', 'Hazyview', 'White River', 'Lydenburg', 'Piet Retief', 'Other'],
86        'North West': ['Mahikeng', 'Rustenburg', 'Klerksdorp', 'Potchefstroom', 'Brits', 'Lichtenburg', 'Vryburg', 'Orkney', 'Stilfontein', 'Wolmaransstad', 'Other'],
87        'Northern Cape': ['Kimberley', 'Upington', 'Springbok', 'De Aar', 'Kuruman', 'Calvinia', 'Colesberg', 'Kathu', 'Postmasburg', 'Prieska', 'Other']
88    };
89
90    //function populateCountryField() {
91    //    var sel = document.getElementById('fx_country');
92    //    if (!sel) return;
93    //    var frag = document.createDocumentFragment();
94    //    COUNTRIES.forEach(function (name) {
95    //        var opt = document.createElement('option');
96    //        opt.value = name;
97    //        opt.textContent = name;
98    //        frag.appendChild(opt);
99    //    });
100    //    sel.appendChild(frag);
101    //    if (typeof $ !== 'undefined' && $.fn.select2) {
102    //        $('#fx_country').select2({ width: '100%' });
103    //    }
104    //}
105
106    function populateCountryField() {
107        var sel = document.getElementById('fx_country');
108        sel.setAttribute('aria-busy', 'true');
109        fetchCountryList().then(function (data) {
110            sel.innerHTML = '<option value="-1">Select...</option>';
111            data.forEach(function (item) {
112                var opt = document.createElement('option');
113                opt.value = item.id;
114                opt.textContent = item.value;
115                sel.appendChild(opt);
116            });
117            sel.disabled = false;
118            sel.removeAttribute('aria-busy');
119            announce('Countries loaded.');
120
121            // Default to South Africa and trigger change
122            var saOption = Array.from(sel.options).find(opt => opt.text.toLowerCase() === 'south africa');
123            if (saOption) {
124                sel.value = saOption.value;
125            } else {
126                sel.value = SOUTH_AFRICA_COUNTRY_ID; // Fallback to ID
127            }
128
129            if (typeof $ !== 'undefined' && $.fn.select2) {
130                $('#fx_country').select2({ width: '100%' });
131                $('#fx_country').off('change.faux').on('change.faux', onCountryChange);
132                // Trigger change after select2 is initialized
133                $('#fx_country').trigger('change.faux');
134            } else {
135                // For non-select2, dispatch a native event
136                sel.dispatchEvent(new Event('change'));
137            }
138
139        }).catch(function (e) {
140            console.error('Country list fetch error:', e);
141            sel.innerHTML = '<option value="-1">Could not load countries</option>';
142            sel.removeAttribute('aria-busy');
143        });
144    }
145
146    // ============================================================
147    // Core helpers
148    // ============================================================
149
150    function syncToUmbracoForm(associateName, value) {
151        var el = document.querySelector('.umbraco-forms-form [data-name="' + associateName + '"]');
152        if (el) el.value = value;
153    }
154
155    function resetSelect(id, preserveOptions) {
156        var sel = document.getElementById(id);
157        if (!sel) return;
158        if (!preserveOptions) {
159            sel.innerHTML = '<option value="-1">Select...</option>';
160        }
161        sel.value = '-1';
162        sel.disabled = true;
163    }
164
165    function populateSelect(id, data, emptyLabel) {
166        var sel = document.getElementById(id);
167        if (!sel) return;
168        if (typeof $ !== 'undefined' && $.fn.select2 && $(sel).data('select2')) {
169            $(sel).select2('destroy');
170        }
171        sel.innerHTML = '<option value="-1">' + (emptyLabel || 'Select...') + '</option>';
172        data.forEach(function (item) {
173            var opt = document.createElement('option');
174            opt.value = item.id;
175            opt.textContent = item.value;
176            sel.appendChild(opt);
177        });
178    }
179
180    // Hoisted so onCountryChange / onProvinceChange can call it after DOM updates.
181    // initSubmitGating assigns the real implementation once the submit button exists.
182    var updateSubmitState = function () { };
183
184    function show(id) {
185        var el = document.getElementById(id);
186        if (el) el.style.display = 'block';
187    }
188
189    function hide(id) {
190        var el = document.getElementById(id);
191        if (el) el.style.display = 'none';
192    }
193
194    function announce(msg) {
195        var el = document.getElementById('faux-form-announcer');
196        if (!el) return;
197        el.textContent = '';
198        setTimeout(function () { el.textContent = msg; }, 50);
199    }
200
201    function setRequiredIndicator(wrapperId, required) {
202        var wrapper = document.getElementById(wrapperId);
203        if (!wrapper) return;
204        var label = wrapper.querySelector('label');
205        if (!label) return;
206        var existing = label.querySelector('.required-indicator');
207        if (required && !existing) {
208            var s = document.createElement('span');
209            s.className = 'text-danger required-indicator';
210            s.setAttribute('aria-hidden', 'true');
211            s.textContent = ' *';
212            label.appendChild(s);
213        } else if (!required && existing) {
214            existing.remove();
215        }
216    }
217
218    // Holds the last fetched programme list so programGroupName can be looked up on selection
219    var lastProgrammesData = [];
220
221    // Whitelist of intake years to show, with the Mode of Delivery options available
222    // for each year. modes: [1] = Contact only, [2] = Distance only, [1,2] = both.
223    // Add/remove entries or adjust modes here when the CRM is out of date.
224    //const INTAKE_YEAR_WHITELIST = [
225    //    { id: 23, modes: [2] },  // 2026-Jul — Contact and Distance
226    //    { id: 22, modes: [1, 2] }   // 2027     — Contact and Distance
227    //];
228
229    // If there are any programs that should be hidden from the list regardless of filters, add their IDs here.
230    //const PROGRAM_BLACKLIST = [];
231
232    //// If there are any campuses that should be hidden from the list regardless of filters, add their IDs here.
233    //const CAMPUS_BLACKLIST = [162];
234
235    //const CAMPUS_DISTANCE_BLACKLIST = [];
236
237    // ============================================================
238    // API fetch functions
239    // ============================================================
240
241    async function fetchIntakeYears() {
242        var r = await fetch('/umbraco/surface/simssurface/getintakeyear');
243        if (!r.ok) throw new Error('Intake year fetch failed (' + r.status + ')');
244        var data = await r.json();
245        // Apply whitelist — only keep items whose id appears in INTAKE_YEAR_WHITELIST
246        //come back and add check if no intake year whitelist then default show all intake years
247        return data.filter(function (item) {
248            return INTAKE_YEAR_WHITELIST.some(function (entry) { return entry.id === item.id; });
249        });
250    }
251
252    async function fetchCampusList() {
253        console.log('Fetching campus list...');
254        var r = await fetch('/umbraco/surface/simssurface/getcampuslist');
255        if (!r.ok) throw new Error('Campus list fetch failed (' + r.status + ')');
256        return r.json();
257    }
258
259    async function fetchProgrammes(intakeYearId, modeOfDeliveryId, locationId, regId, modeOfDelivery) {
260        console.log('Fetching programmes with params:', {
261            intakeYearId: intakeYearId,
262            modeOfDeliveryId: modeOfDeliveryId,
263            locationId: locationId,
264            regId: regId,
265            modeOfDelivery: modeOfDelivery
266        });
267        var url = '/umbraco/surface/simssurface/getquals'
268            + '?intakeYearId=' + encodeURIComponent(intakeYearId)
269            + '&modeOfDeliveryId=' + encodeURIComponent(modeOfDeliveryId)
270            + '&locationId=' + encodeURIComponent(locationId)
271            + '&regId=' + encodeURIComponent(regId)
272            + '&modeOfDelivery=' + encodeURIComponent(modeOfDelivery);
273        var r = await fetch(url);
274        if (!r.ok) throw new Error('Programmes fetch failed (' + r.status + ')');
275        return r.json();
276    }
277
278    async function fetchSchoolList() {
279        console.log('Fetching school list...');
280        var r = await fetch('/umbraco/surface/simssurface/getschoollist');
281        if (!r.ok) throw new Error('School list fetch failed (' + r.status + ')');
282        return r.json();
283    }
284
285    //country,province,city fetches
286    async function fetchCountryList() {
287        console.log('Fetching country list...');
288        var r = await fetch('/umbraco/surface/simssurface/getcountrylist');
289        if (!r.ok) throw new Error('Countries list fetch failed (' + r.status + ')');
290        return r.json();
291    }
292
293    async function fetchProvinceList(countryId) {
294        console.log('Fetching provinces with params:', {
295            countryId: countryId
296        });
297        var url = '/umbraco/surface/simssurface/getcountryregionlist'
298            + '?countryId=' + encodeURIComponent(countryId);
299        var r = await fetch(url);
300        if (!r.ok) throw new Error('Provinces fetch failed (' + r.status + ')');
301        return r.json();
302    }
303
304    async function fetchCityList(countryId, countryRegionId) {
305        console.log('Fetching cities with params:', {
306            countryId: countryId,
307            countryRegionId: countryRegionId
308        });
309        var url = '/umbraco/surface/simssurface/getcitylist'
310            + '?countryId=' + encodeURIComponent(countryId)
311            + '&countryRegionId=' + encodeURIComponent(countryRegionId);
312        var r = await fetch(url);
313        if (!r.ok) throw new Error('Cities fetch failed (' + r.status + ')');
314        return r.json();
315    }
316
317    // ============================================================
318    // State accessors
319    // ============================================================
320
321    function getIntakeYearId() { return document.getElementById('fx_intakeYear').value; }
322    function getModeOfDeliveryId() { return document.getElementById('fx_modeOfDelivery').value; }
323    function getModeOfDeliveryText() {
324        var s = document.getElementById('fx_modeOfDelivery');
325        return (s.selectedIndex >= 0) ? s.options[s.selectedIndex].text : '';
326    }
327    function getRegId() { return document.getElementById('fx_registrationType').value; }
328
329    // ============================================================
330    // Cascade handlers
331    // ============================================================
332
333    function onIntakeYearChange() {
334        var val = getIntakeYearId();
335
336        resetSelect('fx_modeOfDelivery', false); // rebuilt from whitelist below
337        resetSelect('fx_registrationType', true);  // static options — preserve
338        resetSelect('fx_campus');                   // dynamic — clear
339        resetSelect('fx_examCenter', true);  // static options — preserve
340        resetSelect('fx_qualifications');                // dynamic — clear
341        hide('fx_examCenter_wrapper');
342
343        syncToUmbracoForm('intakeYear', val !== '-1' ? val : '');
344
345        if (val !== '-1') {
346            var whitelistEntry = INTAKE_YEAR_WHITELIST.find(function (e) { return e.id === parseInt(val, 10); });
347            var modeSel = document.getElementById('fx_modeOfDelivery');
348            var modeLabels = { 1: 'Contact', 2: 'Distance' };
349            if (whitelistEntry) {
350                whitelistEntry.modes.forEach(function (modeId) {
351                    var opt = document.createElement('option');
352                    opt.value = modeId;
353                    opt.textContent = modeLabels[modeId] || modeId;
354                    modeSel.appendChild(opt);
355                });
356            }
357            modeSel.disabled = false;
358        }
359    }
360
361    function onModeOfDeliveryChange() {
362        var val = getModeOfDeliveryId();
363
364        resetSelect('fx_registrationType', true);  // static options — preserve
365        resetSelect('fx_campus');                   // dynamic — clear
366        resetSelect('fx_examCenter', true);  // static options — preserve
367        resetSelect('fx_qualifications');                // dynamic — clear
368        hide('fx_examCenter_wrapper');
369
370        syncToUmbracoForm('modeOfDelivery', val !== '-1' ? val : '');
371
372        if (val !== '-1') {
373            document.getElementById('fx_registrationType').disabled = false;
374        }
375    }
376
377    function onRegistrationTypeChange() {
378        var regVal = getRegId();
379        var modeVal = getModeOfDeliveryId();
380
381        console.log('[faux] onRegistrationTypeChange — regVal:', regVal, '| modeVal:', modeVal);
382
383        resetSelect('fx_campus');                   // dynamic — clear
384        resetSelect('fx_examCenter', true);  // static options — preserve
385        resetSelect('fx_qualifications');                // dynamic — clear
386        hide('fx_examCenter_wrapper');
387
388        syncToUmbracoForm('registrationType', regVal !== '-1' ? regVal : '');
389        syncToUmbracoForm('campus', '');
390        syncToUmbracoForm('examCenter', '');
391
392        if (regVal === '-1') return;
393
394        // Campus is always fetched regardless of mode.
395        // Distance mode: only EM Online (id 153) is a valid campus.
396        var campusEl = document.getElementById('fx_campus');
397        campusEl.setAttribute('aria-busy', 'true');
398        fetchCampusList().then(function (data) {
399            var campusData = modeVal === '2'
400                ? data.filter(function (item) { return CAMPUS_DISTANCE_BLACKLIST.lastIndexOf(item.id) === -1; })  //{ return item.id === 153; })
401                : (regVal === '1' ? data.filter(function (item) { return CAMPUS_CONTACT_PT_BLACKLIST.indexOf(item.id) === -1; }) : data.filter(function (item) { return CAMPUS_BLACKLIST.indexOf(item.id) === -1; }));
402            console.log('[faux] Campus list received — item count:', campusData.length, '| first item:', campusData[0]);
403            populateSelect('fx_campus', campusData);
404            campusEl.disabled = false;
405            campusEl.removeAttribute('aria-busy');
406            announce('Campus options loaded. Please select a campus.');
407            if (typeof $ !== 'undefined' && $.fn.select2) {
408                $('#fx_campus').select2({ width: '100%' });
409                // Select2 fires jQuery change, not native — bind here after init
410                $('#fx_campus').off('change.faux').on('change.faux', function () {
411                    console.log('[faux] campus Select2 change — value:', $(this).val());
412                    onCampusChange($(this).val());
413                });
414                $('#fx_campus').off('change.validation').on('change.validation', function () {
415                    validateSelect(document.getElementById('fx_campus'));
416                });
417            }
418        }).catch(function (e) {
419            console.error('[faux] Campus list error:', e);
420            campusEl.removeAttribute('aria-busy');
421        });
422    }
423
424    function onCampusChange(locationId) {
425        console.log('[faux] onCampusChange — locationId:', locationId,
426            '| intakeYearId:', getIntakeYearId(), '| modeOfDeliveryId:', getModeOfDeliveryId(),
427            '| regId:', getRegId(), '| modeOfDeliveryText:', getModeOfDeliveryText());
428
429        resetSelect('fx_qualifications');
430        lastProgrammesData = [];
431        syncToUmbracoForm('campus', locationId !== '-1' ? locationId : '');
432        syncToUmbracoForm('programGroupName', '');
433
434        // ExamCenter only visible when campus = EM Online
435        var campusSel = document.getElementById('fx_campus');
436        var selectedText = (campusSel && campusSel.selectedIndex >= 0)
437            ? campusSel.options[campusSel.selectedIndex].text : '';
438        var isEmOnline = selectedText.toLowerCase().indexOf('em online') !== -1;
439
440        if (isEmOnline) {
441            show('fx_examCenter_wrapper');
442            setRequiredIndicator('fx_examCenter_wrapper', true);
443            document.getElementById('fx_examCenter').setAttribute('aria-required', 'true');
444            document.getElementById('fx_examCenter').disabled = false;
445            announce('Exam Centre field is now required. Please make a selection.');
446        } else {
447            resetSelect('fx_examCenter', true);
448            hide('fx_examCenter_wrapper');
449            setRequiredIndicator('fx_examCenter_wrapper', false);
450            document.getElementById('fx_examCenter').removeAttribute('aria-required');
451            syncToUmbracoForm('examCenter', '');
452        }
453
454        if (locationId === '-1') return;
455
456        var progEl = document.getElementById('fx_qualifications');
457        progEl.setAttribute('aria-busy', 'true');
458        fetchProgrammes(
459            getIntakeYearId(),
460            getModeOfDeliveryId(),
461            locationId,
462            getRegId(),
463            getModeOfDeliveryText()
464        ).then(function (data) {
465            console.log('[faux] Programmes received — item count:', data.length, '| first item:', data[0]);
466            data = data.filter(function (p) { return PROGRAM_BLACKLIST.indexOf(p.id) === -1; });
467            data.sort(function (a, b) { return a.value.localeCompare(b.value); });
468            lastProgrammesData = data;
469            populateSelect('fx_qualifications', data);
470            progEl.disabled = false;
471            progEl.removeAttribute('aria-busy');
472            announce('Programme options loaded. Please select a programme.');
473            if (typeof $ !== 'undefined' && $.fn.select2) {
474                $('#fx_qualifications').select2({ width: '100%' });
475                $('#fx_qualifications').off('change.faux').on('change.faux', function () {
476                    var val = $(this).val();
477                    syncToUmbracoForm('qualifications', val && val !== '-1' ? val : '');
478                    var match = val ? lastProgrammesData.find(function (p) { return String(p.id) === String(val); }) : null;
479                    syncToUmbracoForm('programGroupName', match && match.programGroupName ? match.programGroupName : '');
480                });
481                $('#fx_qualifications').off('change.validation').on('change.validation', function () {
482                    validateSelect(document.getElementById('fx_qualifications'));
483                });
484            }
485        }).catch(function (e) {
486            console.error('Programme fetch error:', e);
487            progEl.removeAttribute('aria-busy');
488        });
489    }
490
491    //function onCountryChange() {
492    //    var val = document.getElementById('fx_country').value;
493
494    //    // Reset and hide province + city
495    //    var provSel = document.getElementById('fx_province');
496    //    if (provSel) { provSel.innerHTML = '<option value="-1">Select...</option>'; provSel.value = '-1'; provSel.disabled = true; }
497    //    var citySel = document.getElementById('fx_city');
498    //    if (citySel) { citySel.innerHTML = '<option value="-1">Select...</option>'; citySel.value = '-1'; citySel.disabled = true; }
499    //    //hide('fx_province_wrapper');
500    //    //setRequiredIndicator('fx_province_wrapper', false);
501    //    //hide('fx_city_wrapper');
502    //    //setRequiredIndicator('fx_city_wrapper', false);
503
504    //    syncToUmbracoForm('country',  val !== '-1' ? val : '');
505    //    syncToUmbracoForm('province', '');
506    //    syncToUmbracoForm('city',     '');
507
508    //    if (val === 'South Africa') {
509    //        if (provSel) {
510    //            var frag = document.createDocumentFragment();
511    //            Object.keys(PROVINCE_CITY_MAP).forEach(function (prov) {
512    //                var opt = document.createElement('option');
513    //                opt.value = prov;
514    //                opt.textContent = prov;
515    //                frag.appendChild(opt);
516    //            });
517    //            provSel.appendChild(frag);
518    //            provSel.disabled = false;
519    //        }
520    //        show('fx_province_wrapper');
521    //        setRequiredIndicator('fx_province_wrapper', true);
522    //        announce('Province field is now available.');
523    //    } else if (val && val !== '-1') {
524    //        // Non-SA country — pre-populate province and city with "Other"
525    //        if (provSel) {
526    //            provSel.innerHTML = '<option value="Other">Other</option>';
527    //            provSel.value = 'Other';
528    //            provSel.disabled = false;
529    //        }
530    //        if (citySel) {
531    //            citySel.innerHTML = '<option value="Other">Other</option>';
532    //            citySel.value = 'Other';
533    //            citySel.disabled = false;
534    //        }
535    //        //show('fx_province_wrapper');
536    //        //show('fx_city_wrapper');
537    //        syncToUmbracoForm('province', 'Other');
538    //        syncToUmbracoForm('city',     'Other');
539    //    }
540    //    updateSubmitState();
541    //}
542
543    function onCountryChange() {
544        var val = document.getElementById('fx_country').value;
545
546        // Reset province and city
547        resetSelect('fx_province');
548        resetSelect('fx_city');
549        hide('fx_province_wrapper');
550        hide('fx_city_wrapper');
551        setRequiredIndicator('fx_province_wrapper', false);
552        setRequiredIndicator('fx_city_wrapper', false);
553
554        syncToUmbracoForm('country', val !== '-1' ? val : '');
555        syncToUmbracoForm('province', '');
556        syncToUmbracoForm('city', '');
557
558        if (val === '-1') {
559            updateSubmitState();
560            return;
561        }
562
563        var provSel = document.getElementById('fx_province');
564        show('fx_province_wrapper');
565        setRequiredIndicator('fx_province_wrapper', true);
566        provSel.setAttribute('aria-busy', 'true');
567        announce('Loading provinces...');
568
569        fetchProvinceList(val).then(function (data) {
570            if (data && data.length > 0) {
571                if (data.length == 1) {
572                    syncToUmbracoForm('province', data[0].id);
573                    hide('fx_province_wrapper');
574                    setRequiredIndicator('fx_province_wrapper', false);
575                    fetchCityList(val, data[0].id).then(function (d) {
576                        var citySel = document.getElementById('fx_city');
577                        show('fx_city_wrapper');
578                        setRequiredIndicator('fx_city_wrapper', true);
579                        citySel.setAttribute('aria-busy', 'true');
580                        if (d && d.length > 0) {
581                            if (d.length == 1) {
582                                syncToUmbracoForm('city', d[0].id);
583                                hide('fx_city_wrapper');
584                                setRequiredIndicator('fx_city_wrapper', false);
585                            }
586                            else {
587
588                                populateSelect('fx_city', d);
589                                citySel.disabled = false;
590                                announce('City field is now available.');
591                                citySel.removeAttribute('aria-busy');
592                                updateSubmitState();
593                            }
594                        }
595                    });
596
597
598
599                }
600                else {
601                    populateSelect('fx_province', data);
602                    provSel.disabled = false;
603                    announce('Province field is now available.');
604                }
605
606            } else {
607                // No provinces returned, hide the dropdown and default to 'Other'
608                //adding this line to update the js
609                provSel.innerHTML = '<option value="Other">Other</option>';
610                provSel.value = 'Other';
611                hide('fx_province_wrapper');
612                setRequiredIndicator('fx_province_wrapper', false);
613                // syncToUmbracoForm('province', 'Other');
614                // // Also default city to 'Other' and show it
615                 var citySel = document.getElementById('fx_city');
616                if (citySel) {
617                    citySel.innerHTML = '<option value="Other">Other</option>';
618                    citySel.value = 'Other';
619                    citySel.disabled = false;
620                }
621                // show('fx_city_wrapper');
622                setRequiredIndicator('fx_city_wrapper', false);
623                // syncToUmbracoForm('city', 'Other');
624                // announce('City field is now available.');
625            }
626            provSel.removeAttribute('aria-busy');
627            updateSubmitState();
628        }).catch(function (e) {
629            console.error('Province fetch error:', e);
630            hide('fx_province_wrapper');            
631            provSel.innerHTML = '<option value="Other">Other</option>';
632            provSel.value = 'Other';
633            setRequiredIndicator('fx_province_wrapper', false);
634            // provSel.innerHTML = '<option value="-1">Could not load provinces</option>';
635            provSel.removeAttribute('aria-busy');
636            var citySel = document.getElementById('fx_city');
637            if (citySel) {
638                citySel.innerHTML = '<option value="Other">Other</option>';
639                citySel.value = 'Other';
640                citySel.disabled = false;
641            }
642            setRequiredIndicator('fx_city_wrapper', false);
643            updateSubmitState();
644        });
645    }
646
647
648    //function onProvinceChange() {
649    //    var val = document.getElementById('fx_province').value;
650
651    //    var citySel = document.getElementById('fx_city');
652    //    if (citySel) { citySel.innerHTML = '<option value="-1">Select...</option>'; citySel.value = '-1'; citySel.disabled = true; }
653    //    hide('fx_city_wrapper');
654    //    setRequiredIndicator('fx_city_wrapper', false);
655
656    //    syncToUmbracoForm('province', val !== '-1' ? val : '');
657    //    syncToUmbracoForm('city',     '');
658
659    //    if (val && val !== '-1' && PROVINCE_CITY_MAP[val]) {
660    //        if (citySel) {
661    //            var frag = document.createDocumentFragment();
662    //            PROVINCE_CITY_MAP[val].forEach(function (city) {
663    //                var opt = document.createElement('option');
664    //                opt.value = city;
665    //                opt.textContent = city;
666    //                frag.appendChild(opt);
667    //            });
668    //            citySel.appendChild(frag);
669    //            citySel.disabled = false;
670    //        }
671    //        show('fx_city_wrapper');
672    //        setRequiredIndicator('fx_city_wrapper', true);
673    //        announce('City field is now available.');
674    //    }
675    //    updateSubmitState();
676    //}
677
678    function onProvinceChange() {
679        var provinceId = document.getElementById('fx_province').value;
680        var countryId = document.getElementById('fx_country').value;
681
682        // Reset city
683        resetSelect('fx_city');
684        hide('fx_city_wrapper');
685        setRequiredIndicator('fx_city_wrapper', false);
686
687        syncToUmbracoForm('province', provinceId !== '-1' ? provinceId : '');
688        syncToUmbracoForm('city', '');
689
690        if (provinceId === '-1') {
691            updateSubmitState();
692            return;
693        }
694
695        var citySel = document.getElementById('fx_city');
696        show('fx_city_wrapper');
697        setRequiredIndicator('fx_city_wrapper', true);
698        citySel.setAttribute('aria-busy', 'true');
699        announce('Loading cities...');
700
701        fetchCityList(countryId, provinceId).then(function (data) {
702            if (data && data.length > 0) {
703                populateSelect('fx_city', data);
704                citySel.disabled = false;
705                announce('City field is now available.');
706            } else {
707                // No cities returned, provide 'Other' as the only option.
708                citySel.innerHTML = '<option value="Other">Other</option>';
709                citySel.value = 'Other';
710                citySel.disabled = false;
711                syncToUmbracoForm('city', 'Other');
712                announce('City field is now available.');
713            }
714            citySel.removeAttribute('aria-busy');
715            updateSubmitState();
716        }).catch(function (e) {
717            console.error('City fetch error:', e);
718            citySel.innerHTML = '<option value="-1">Could not load cities</option>';
719            citySel.removeAttribute('aria-busy');
720            updateSubmitState();
721        });
722    }
723
724    // ============================================================
725    // Field sync listeners (non-cascade fields)
726    // ============================================================
727
728    function attachFieldSyncListeners() {
729        ['fx_title', 'fx_parentTitle'].forEach(function (id) {
730            var el = document.getElementById(id);
731            if (!el) return;
732            el.addEventListener('change', function () {
733                syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.value !== '-1' ? el.value : '');
734            });
735        });
736
737        ['fx_finance'].forEach(function (id) {
738            var el = document.getElementById(id);
739            if (!el) return;
740            el.addEventListener('change', function () {
741                if (el.value === '146') { // value "146" = "NSFAS"
742                    show('nsfas-message');
743                } else {
744                    hide('nsfas-message');
745                }
746
747                syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.value !== '-1' ? el.options[el.selectedIndex].text : '');
748            });
749        });
750
751        document.getElementById('fx_qualifications').addEventListener('change', function () {
752            var val = this.value !== '-1' ? this.value : '';
753            syncToUmbracoForm('qualifications', val);
754            var match = val ? lastProgrammesData.find(function (p) { return String(p.id) === String(val); }) : null;
755            syncToUmbracoForm('programGroupName', match && match.programGroupName ? match.programGroupName : '');
756        });
757
758        [
759            'fx_firstName', 'fx_middleNames', 'fx_surname', 'fx_emailAddress',
760            'fx_mobileNumber', 'fx_altPhoneNumber', 'fx_nationalIdNumberPassport',
761            'fx_parentFirstName', 'fx_parentSurname', 'fx_parentMobileNumber', 'fx_parentEmailAddress'
762        ].forEach(function (id) {
763            var el = document.getElementById(id);
764            if (!el) return;
765            el.addEventListener('blur', function () {
766                syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.value);
767            });
768        });
769
770        document.getElementById('fx_comment').addEventListener('blur', function () {
771            syncToUmbracoForm('comment', this.value);
772        });
773
774        document.querySelectorAll('input[name="fx_southAfricanCitizen"]').forEach(function (radio) {
775            radio.addEventListener('change', function () {
776                if (this.checked) syncToUmbracoForm('southAfricanCitizen', this.value);
777                // Re-validate ID field when citizenship changes — format rules differ
778                var idEl = document.getElementById('fx_nationalIdNumberPassport');
779                if (idEl && idEl.value.trim()) validateInput(idEl);
780            });
781        });
782
783        document.querySelectorAll('.faux-brand-check').forEach(function (cb) {
784            cb.addEventListener('change', function () {
785                var vals = Array.from(document.querySelectorAll('.faux-brand-check:checked'))
786                    .map(function (c) { return c.value; });
787                syncToUmbracoForm('mediaExposure', vals.join(','));
788            });
789        });
790
791        // Country code selects use plain native change (no Select2)
792        ['fx_mobileNumberCode', 'fx_altPhoneNumberCode', 'fx_parentMobileNumberCode'].forEach(function (id) {
793            var el = document.getElementById(id);
794            if (!el) return;
795            el.addEventListener('change', function () {
796                syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.value);
797            });
798        });
799
800        // Toggle "Other" education text input
801        function onEducationLevelChange(val) {
802            var otherInput = document.getElementById('fx_highestEducationOther');
803            if (val === '3') { // value "3" = "Other"
804                show('fx_highestEducationOther_wrapper');
805            } else {
806                hide('fx_highestEducationOther_wrapper');
807                if (otherInput) otherInput.value = '';
808            }
809        }
810
811        // Select2-powered selects — must use jQuery change
812        var select2SyncIds = ['fx_yearOfCompletion', 'fx_schoolInstitution', 'fx_highestEducationLevelCompleted'];
813        if (typeof $ !== 'undefined' && $.fn.select2) {
814            select2SyncIds.forEach(function (id) {
815                var el = document.getElementById(id);
816                if (!el) return;
817                $(el).on('change', function () {
818                    if (id === 'fx_schoolInstitution' && this.value !== '-1') {
819                        syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.options[el.selectedIndex].text);
820                    }
821                    else {
822                        var val = $(this).val();
823                        syncToUmbracoForm(el.name.replace(/^fx_/, ''), val !== '-1' ? val : '');
824                    }
825                    if (id === 'fx_highestEducationLevelCompleted') onEducationLevelChange(val);
826                });
827            });
828        } else {
829            select2SyncIds.forEach(function (id) {
830                var el = document.getElementById(id);
831                if (!el) return;
832                el.addEventListener('change', function () {
833                    syncToUmbracoForm(el.name.replace(/^fx_/, ''), el.value !== '-1' ? el.value : '');
834                    if (id === 'fx_highestEducationLevelCompleted') onEducationLevelChange(el.value);
835                });
836            });
837        }
838    }
839
840    // ============================================================
841    // Validation
842    // ============================================================
843
844    function getValidationSpan(el) {
845        var col = el.closest('.col-12');
846        return col ? col.querySelector('.field-validation-error') : null;
847    }
848
849    function getLabelForErrorSpan(span) {
850        var col = span.closest('.col-12');
851        if (!col) return '';
852        var labelEl = col.querySelector('legend.form-label') || col.querySelector('label.form-label');
853        if (!labelEl) return '';
854        return labelEl.textContent.replace(/\s*\*\s*$/, '').trim();
855    }
856
857    function updateValidationRecap() {
858        var recap = document.querySelector('.umbraco-forms-navigation .validation-recap');
859        if (!recap) return;
860
861        var visibleErrors = Array.from(
862            document.querySelectorAll('#faux-enquiry-form .field-validation-error')
863        ).filter(function (span) {
864            return span.offsetParent !== null;
865        });
866
867        if (visibleErrors.length === 0) {
868            recap.style.display = 'none';
869            recap.innerHTML = '';
870            return;
871        }
872
873        var html = '<p>Please correct the following:</p><ul>';
874        visibleErrors.forEach(function (span) {
875            var label = getLabelForErrorSpan(span);
876            var msg = span.textContent.trim();
877            var errorId = span.id || '';
878            html += '<li class="validation-recap-item" data-error-id="' + errorId + '">'
879                + (label ? label + ': ' + msg : msg)
880                + '</li>';
881        });
882        html += '</ul>';
883        recap.innerHTML = html;
884
885        recap.querySelectorAll('.validation-recap-item').forEach(function (li) {
886            li.addEventListener('click', function () {
887                var errorId = li.getAttribute('data-error-id');
888                var target = errorId
889                    ? document.querySelector('[aria-describedby="' + errorId + '"]')
890                    : null;
891                // Fallback: scroll to the error span itself
892                if (!target && errorId) target = document.getElementById(errorId);
893                if (target) {
894                    target.scrollIntoView({ behavior: 'smooth', block: 'center' });
895                    if (typeof target.focus === 'function') target.focus({ preventScroll: true });
896                }
897            });
898        });
899
900        recap.style.display = 'block';
901    }
902
903    function showError(span) {
904        if (!span) return;
905        span.style.display = '';
906        var input = span.id ? document.querySelector('[aria-describedby="' + span.id + '"]') : null;
907        if (input) input.setAttribute('aria-invalid', 'true');
908        updateValidationRecap();
909    }
910    function hideError(span) {
911        if (!span) return;
912        span.style.display = 'none';
913        var input = span.id ? document.querySelector('[aria-describedby="' + span.id + '"]') : null;
914        if (input) input.removeAttribute('aria-invalid');
915        updateValidationRecap();
916    }
917
918    function validateSelect(el) {
919        var span = getValidationSpan(el);
920        var invalid = !el.value || el.value === '-1';
921        invalid ? showError(span) : hideError(span);
922        return !invalid;
923    }
924
925    function isValidEmail(value) {
926        // This regex provides a good balance of correctness and simplicity for web forms.
927        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
928        return emailRegex.test(value);
929    }
930
931    const RSA_ID_REGEX = /^(([0-9]{2})(0[1-9]|1[0-2])([0-2][1-9]|[1-2][0-9]|3[01]))([ ]?)(([0-9]{4})([ ]?)([0-1][8]([ ]?)[0-9]))$/;
932
933    function isSouthAfricanCitizen() {
934        var checked = document.querySelector('input[name="fx_southAfricanCitizen"]:checked');
935        if (checked.value === '1') {
936            syncToUmbracoForm('southAfricanCitizen', checked.value);
937        }
938        return checked && checked.value === '1';
939    }
940
941    function isValidSAPassport(passportNumber) {
942        // Checks for a letter followed by 8 digits.
943        const PASSPORT_REGEX = /^[A-Z][0-9]{8}$/i;
944        return PASSPORT_REGEX.test(passportNumber);
945    }
946
947    function validateInput(el) {
948        var span = getValidationSpan(el);
949        var value = el.value.trim();
950        var invalid = !value;
951        var message = 'This field is required';
952
953        if (!invalid && el.type === 'email') {
954            invalid = !isValidEmail(value);
955            if (invalid) message = 'Please enter a valid email address';
956        } else if (el.id === 'fx_nationalIdNumberPassport' && isSouthAfricanCitizen()) {
957            if (value.length === 0) {
958                invalid = true;
959                message = 'This field is required';
960            } else if (/^[a-zA-Z]/.test(value)) {
961                // Starts with a letter, so validate as a passport
962                invalid = !isValidSAPassport(value);
963                if (invalid) message = 'Please enter a valid passport number (e.g., M12345678).';
964            } else {
965                // Otherwise, validate as a South African ID number
966                invalid = !RSA_ID_REGEX.test(value);
967                if (invalid) message = 'Please enter a valid 13-digit South African ID number.';
968            }
969        }
970        //invalid = !RSA_ID_REGEX.test(value);
971        //if (invalid) message = 'Please enter a valid South African ID number';
972        //} else if (!invalid && el.id === 'fx_nationalIdNumberPassport' && !isSouthAfricanCitizen()) {
973        //    invalid = !isValidSAPassport(value);
974        //    if (invalid) message = 'Please enter a valid ID or Passport number';
975        //}
976
977        if (span) span.textContent = invalid ? message : 'This field is required';
978        invalid ? showError(span) : hideError(span);
979        return !invalid;
980    }
981
982    function initValidation() {
983        // Hide all validation spans on load
984        document.querySelectorAll('#faux-enquiry-form .field-validation-error').forEach(function (span) {
985            span.style.display = 'none';
986        });
987
988        // Required selects — validate on change
989        ['fx_intakeYear', 'fx_modeOfDelivery', 'fx_registrationType', 'fx_title', 'fx_campus'].forEach(function (id) {
990            var el = document.getElementById(id);
991            if (el) el.addEventListener('change', function () { validateSelect(el); });
992        });
993
994        var examSel = document.getElementById('fx_examCenter');
995        if (examSel) examSel.addEventListener('change', function () {
996            if (document.getElementById('fx_examCenter_wrapper').style.display !== 'none') validateSelect(examSel);
997        });
998
999        var programmeSel = document.getElementById('fx_qualifications');
1000        if (programmeSel) programmeSel.addEventListener('change', function () { validateSelect(programmeSel); });
1001
1002        var countrySel = document.getElementById('fx_country');
1003        if (countrySel) {
1004            if (typeof $ !== 'undefined' && $.fn.select2) {
1005                $(countrySel).on('change.validation', function () { validateSelect(countrySel); });
1006            } else {
1007                countrySel.addEventListener('change', function () { validateSelect(countrySel); });
1008            }
1009        }
1010
1011        var provinceSel = document.getElementById('fx_province');
1012        if (provinceSel) provinceSel.addEventListener('change', function () {
1013            if (document.getElementById('fx_province_wrapper').style.display !== 'none') validateSelect(prov
1013inceSel);
1014        });
1015
1016        var citySel2 = document.getElementById('fx_city');
1017        if (citySel2) citySel2.addEventListener('change', function () {
1018            syncToUmbracoForm('city', citySel2.value !== '-1' ? citySel2.value : '');
1019            if (document.getElementById('fx_city_wrapper').style.display !== 'none') validateSelect(citySel2);
1020        });
1021
1022        // Required text inputs — validate on blur
1023        ['fx_firstName', 'fx_surname', 'fx_emailAddress', 'fx_nationalIdNumberPassport'].forEach(function (id) {
1024            var el = document.getElementById(id);
1025            if (el) el.addEventListener('blur', function () { validateInput(el); });
1026        });
1027
1028        // Parent email — not required but still validated for format on blur
1029        var parentEmailEl = document.getElementById('fx_parentEmailAddress');
1030        if (parentEmailEl) parentEmailEl.addEventListener('blur', function () {
1031            var span = getValidationSpan(parentEmailEl);
1032            var value = parentEmailEl.value.trim();
1033            if (value && !isValidEmail(value)) {
1034                if (span) span.textContent = 'Please enter a valid email address';
1035                showError(span);
1036            } else {
1037                hideError(span);
1038            }
1039        });
1040
1041        // Mobile number — shared span sits on the parent col-12, above the phone group
1042        var mobileInput = document.getElementById('fx_mobileNumber');
1043        if (mobileInput) mobileInput.addEventListener('blur', function () { validateInput(mobileInput); });
1044
1045        // Media exposure checkboxes — validate live on every check/uncheck
1046        var mediaSpan = document.getElementById('err_mediaExposure');
1047        if (mediaSpan) {
1048            document.querySelectorAll('.form-check-input.faux-brand-check').forEach(function (cb) {
1049                cb.addEventListener('change', function () {
1050                    var anyChecked = !!document.querySelector('.form-check-input.faux-brand-check:checked');
1051                    anyChecked ? hideError(mediaSpan) : showError(mediaSpan);
1052                });
1053            });
1054        }
1055    }
1056
1057    function validateAllFields() {
1058        // Required selects
1059        ['fx_intakeYear', 'fx_modeOfDelivery', 'fx_registrationType', 'fx_title', 'fx_campus', 'fx_qualifications', 'fx_country'].forEach(function (id) {
1060            var el = document.getElementById(id);
1061            if (el) validateSelect(el);
1062        });
1063
1064        // Conditional selects — only validate when their wrapper is visible
1065        [
1066            { id: 'fx_examCenter', wrapperId: 'fx_examCenter_wrapper' },
1067            { id: 'fx_province', wrapperId: 'fx_province_wrapper' },
1068            { id: 'fx_city', wrapperId: 'fx_city_wrapper' }
1069        ].forEach(function (pair) {
1070            var wrapper = document.getElementById(pair.wrapperId);
1071            if (wrapper && wrapper.style.display !== 'none') {
1072                var el = document.getElementById(pair.id);
1073                if (el) validateSelect(el);
1074            }
1075        });
1076
1077        // Required text inputs
1078        ['fx_firstName', 'fx_surname', 'fx_emailAddress', 'fx_mobileNumber', 'fx_nationalIdNumberPassport'].forEach(function (id) {
1079            var el = document.getElementById(id);
1080            if (el) validateInput(el);
1081        });
1082
1083        // Parent email — format only, not required
1084        var parentEmailEl = document.getElementById('fx_parentEmailAddress');
1085        if (parentEmailEl && parentEmailEl.value.trim()) {
1086            var peSpan = getValidationSpan(parentEmailEl);
1087            if (!isValidEmail(parentEmailEl.value.trim())) {
1088                if (peSpan) peSpan.textContent = 'Please enter a valid email address';
1089                showError(peSpan);
1090            } else {
1091                hideError(peSpan);
1092            }
1093        }
1094
1095        // Media exposure checkboxes
1096        var mediaSpan = document.getElementById('err_mediaExposure');
1097        if (mediaSpan) {
1098            var anyBrandChecked = !!document.querySelector('.form-check-input.faux-brand-check:checked');
1099            anyBrandChecked ? hideError(mediaSpan) : showError(mediaSpan);
1100        }
1101    }
1102
1103    // ============================================================
1104    // Submit gating — disable submit until faux form complete + T&C checked
1105    // ============================================================
1106
1107    function initSubmitGating() {
1108        var submitBtn = document.querySelector('.umbraco-forms-form input[type="submit"], .umbraco-forms-form button[type="submit"]');
1109        if (!submitBtn) return;
1110
1111        submitBtn.disabled = true;
1112        submitBtn.setAttribute('aria-describedby', 'submit-hint');
1113
1114        function isFormReady() {
1115            // Required selects (value must not be '-1' or empty)
1116            var requiredSelects = ['fx_intakeYear', 'fx_modeOfDelivery', 'fx_registrationType', 'fx_campus', 'fx_qualifications', 'fx_title'];
1117            for (var i = 0; i < requiredSelects.length; i++) {
1118                var sel = document.getElementById(requiredSelects[i]);
1119                if (!sel) continue;
1120                var v = sel.value;
1121                if (!v || v === '-1' || v === '') return false;
1122            }
1123
1124            // ExamCenter only required when EM Online campus is selected
1125            var examWrapper = document.getElementById('fx_examCenter_wrapper');
1126            if (examWrapper && examWrapper.style.display !== 'none') {
1127                var es = document.getElementById('fx_examCenter');
1128                if (!es || !es.value || es.value === '-1' || es.value === '') return false;
1129            }
1130
1131            // Country — always required
1132            var countrySel2 = document.getElementById('fx_country');
1133            if (!countrySel2 || !countrySel2.value || countrySel2.value === '-1') return false;
1134
1135            // Province — required only when South Africa selected (wrapper visible)
1136            var provWrapper = document.getElementById('fx_province_wrapper');
1137            if (provWrapper && provWrapper.style.display !== 'none') {
1138                var ps = document.getElementById('fx_province');
1139                if (!ps || !ps.value || ps.value === '-1') return false;
1140            }
1141
1142            // City — required only when province selected (wrapper visible)
1143            var cityWrapper = document.getElementById('fx_city_wrapper');
1144            if (cityWrapper && cityWrapper.style.display !== 'none') {
1145                var cs2 = document.getElementById('fx_city');
1146                if (!cs2 || !cs2.value || cs2.value === '-1') return false;
1147            }
1148
1149            // Required text inputs
1150            var requiredInputs = ['fx_firstName', 'fx_surname', 'fx_emailAddress', 'fx_mobileNumber', 'fx_nationalIdNumberPassport'];
1151            for (var j = 0; j < requiredInputs.length; j++) {
1152                var inp = document.getElementById(requiredInputs[j]);
1153                if (!inp) continue;
1154                var val = inp.value ? inp.value.trim() : '';
1155                if (!val) return false;
1156                if (inp.id === 'fx_nationalIdNumberPassport' && isSouthAfricanCitizen()) {
1157                    var isPassportFormat = /^[a-zA-Z]/.test(val);
1158                    if (isPassportFormat) {
1159                        // If it looks like a passport, it must be a valid one.
1160                        if (!isValidSAPassport(val)) return false;
1161                    } else {
1162                        // Otherwise, it must be a valid ID number.
1163                        if (!RSA_ID_REGEX.test(val)) return false;
1164                    }
1165                }
1166                if (inp.type === 'email' && !isValidEmail(val)) return false;
1167            }
1168
1169
1170
1171            // T&C checkbox on Umbraco form
1172            var tcCheck = document.querySelector('.umbraco-forms-form input[type="checkbox"]');
1173            if (!tcCheck || !tcCheck.checked) return false;
1174
1175            // Check if at least one "How did you hear about us?" checkbox is checked
1176            var brandCheck = document.querySelector('.form-check-input.faux-brand-check:checked');
1177            if (!brandCheck) return false;
1178
1179            return true;
1180        }
1181
1182        updateSubmitState = function () {
1183            submitBtn.disabled = !isFormReady();
1184        };
1185
1186        // Watch all required selects (native change)
1187        ['fx_intakeYear', 'fx_modeOfDelivery', 'fx_registrationType', 'fx_campus', 'fx_examCenter', 'fx_qualifications', 'fx_title', 'fx_country', 'fx_province', 'fx_city'].forEach(function (id) {
1188            var el = document.getElementById(id);
1189            if (el) el.addEventListener('change', updateSubmitState);
1190        });
1191        // Select2-driven selects also fire jQuery change
1192        if (typeof $ !== 'undefined' && $.fn.select2) {
1193            $('#fx_campus, #fx_country').on('change.submitgate', updateSubmitState);
1194        }
1195
1196        // Watch required text inputs
1197        ['fx_firstName', 'fx_surname', 'fx_emailAddress', 'fx_mobileNumber', 'fx_nationalIdNumberPassport'].forEach(function (id) {
1198            var el = document.getElementById(id);
1199            if (el) el.addEventListener('input', updateSubmitState);
1200        });
1201
1202        // Watch SA citizen radios — switching Yes/No changes ID number validation rules
1203        document.querySelectorAll('input[name="fx_southAfricanCitizen"]').forEach(function (radio) {
1204            radio.addEventListener('change', updateSubmitState);
1205        });
1206
1207        // Watch T&C checkbox
1208        var tcCheck = document.querySelector('.umbraco-forms-form input[type="checkbox"]');
1209        if (tcCheck) {
1210            tcCheck.addEventListener('change', updateSubmitState);
1211            tcCheck.addEventListener('click', validateAllFields);
1212        }
1213
1214        // Watch "How did you hear about us?" checkboxes
1215        document.querySelectorAll('.form-check-input.faux-brand-check').forEach(function (checkbox) {
1216            checkbox.addEventListener('change', updateSubmitState);
1217        });
1218
1219    }
1220
1221    // ============================================================
1222    // Hide Umbraco form's own input fieldsets
1223    // Keeps only the last fieldset visible (T&C checkbox + submit).
1224    // ============================================================
1225
1226    function hideUmbracoFormFields() {
1227        var fieldsets = document.querySelectorAll('.umbraco-forms-form .umbraco-forms-fieldset');
1228        fieldsets.forEach(function (fs, i) {
1229            if (i < fieldsets.length - 1) fs.style.display = 'none';
1230        });
1231    }
1232
1233    // ============================================================
1234    // Select2 — initialise large static dropdowns
1235    // ============================================================
1236
1237    function initSelect2Fields() {
1238        if (typeof $ === 'undefined' || !$.fn.select2) return;
1239        ['#fx_yearOfCompletion'].forEach(function (sel) {
1240            $(sel).select2({ width: '100%' });
1241        });
1242    }
1243
1244    // ============================================================
1245    // Main init
1246    // ============================================================
1247
1248    document.addEventListener('DOMContentLoaded', function () {
1249        ['fx_mobileNumberCode', 'fx_altPhoneNumberCode', 'fx_parentMobileNumberCode'].forEach(populateCountryCodes);
1250
1251        // Restrict phone number and ID/Passport inputs to digits only
1252        ['fx_mobileNumber', 'fx_altPhoneNumber', 'fx_parentMobileNumber'].forEach(function (id) {//, 'fx_nationalIdNumberPassport'
1253            var el = document.getElementById(id);
1254            if (!el) return;
1255            el.addEventListener('input', function () {
1256                this.value = this.value.replace(/[^0-9]/g, '');
1257            });
1258        });
1259
1260        // Enforce maxlength on phone inputs based on country code (+27 → 11, all others → 15) adjusted to 11 digits for +27 to allow for leading zero which is common in SA local numbers
1261        var phonePairs = [
1262            { codeId: 'fx_mobileNumberCode', numberId: 'fx_mobileNumber' },
1263            { codeId: 'fx_altPhoneNumberCode', numberId: 'fx_altPhoneNumber' },
1264            { codeId: 'fx_parentMobileNumberCode', numberId: 'fx_parentMobileNumber' }
1265        ];
1266
1267        function applyPhoneMaxLength(codeEl, numberEl) {
1268            numberEl.maxLength = codeEl.value === '+27' ? 11 : 15;
1269            if (numberEl.value.length > numberEl.maxLength) {
1270                numberEl.value = numberEl.value.slice(0, numberEl.maxLength);
1271            }
1272        }
1273
1274        phonePairs.forEach(function (pair) {
1275            var codeEl = document.getElementById(pair.codeId);
1276            var numberEl = document.getElementById(pair.numberId);
1277            if (!codeEl || !numberEl) return;
1278            applyPhoneMaxLength(codeEl, numberEl);
1279            codeEl.addEventListener('change', function () { applyPhoneMaxLength(codeEl, numberEl); });
1280        });
1281
1282        // Restrict name fields to letters only (no digits)
1283        ['fx_firstName', 'fx_middleNames', 'fx_surname', 'fx_parentFirstName', 'fx_parentSurname'].forEach(function (id) {
1284            var el = document.getElementById(id);
1285            if (!el) return;
1286            el.addEventListener('input', function () {
1287                this.value = this.value.replace(/[0-9]/g, '');
1288            });
1289        });
1290
1291        // Populate year of completion (2026 → 1950)
1292        (function () {
1293            var sel = document.getElementById('fx_yearOfCompletion');
1294            if (!sel) return;
1295            var frag = document.createDocumentFragment();
1296            var placeholder = document.createElement('option');
1297            placeholder.value = '';
1298            placeholder.textContent = 'Select...';
1299            frag.appendChild(placeholder);
1300            for (var y = 2026; y >= 1950; y--) {
1301                var opt = document.createElement('option');
1302                opt.value = y;
1303                opt.textContent = y;
1304                frag.appendChild(opt);
1305            }
1306            sel.appendChild(frag);
1307        }());
1308
1309        populateCountryField();
1310
1311        hideUmbracoFormFields();
1312
1313        (function () {
1314            var navDiv = document.querySelector('.umbraco-forms-navigation');
1315            if (navDiv) {
1316                var recapDiv = document.createElement('div');
1317                recapDiv.className = 'validation-recap alert alert-danger';
1318                recapDiv.style.display = 'none';
1319                navDiv.insertBefore(recapDiv, navDiv.firstChild);
1320            }
1321        }());
1322
1323        initSelect2Fields();
1324        attachFieldSyncListeners();
1325        initValidation();
1326        initSubmitGating();
1327
1328        // Select2 case: change.faux is bound inside populateCountryField() after Select2 init.
1329        // Native fallback for when Select2 is unavailable.
1330        if (typeof $ === 'undefined' || !$.fn.select2) {
1331            document.getElementById('fx_country').addEventListener('change', onCountryChange);
1332        }
1333        document.getElementById('fx_province').addEventListener('change', onProvinceChange);
1334        document.getElementById('fx_intakeYear').addEventListener('change', onIntakeYearChange);
1335        document.getElementById('fx_modeOfDelivery').addEventListener('change', onModeOfDeliveryChange);
1336        document.getElementById('fx_registrationType').addEventListener('change', onRegistrationTypeChange);
1337
1338        document.getElementById('fx_campus').addEventListener('change', function () {
1339            console.log('[faux] campus native change — value:', this.value);
1340            onCampusChange(this.value);
1341        });
1342        document.getElementById('fx_examCenter').addEventListener('change', function () {
1343            syncToUmbracoForm('examCenter', this.value !== '-1' ? this.value : '');
1344        });
1345
1346        (function () {
1347            var sel = document.getElementById('fx_intakeYear');
1348            sel.setAttribute('aria-busy', 'true');
1349            fetchIntakeYears().then(function (data) {
1350                sel.innerHTML = '<option value="-1">Select...</option>';
1351                data.forEach(function (item) {
1352                    var opt = document.createElement('option');
1353                    opt.value = item.id;
1354                    opt.textContent = item.value;
1355                    sel.appendChild(opt);
1356                });
1357                sel.disabled = false;
1358                sel.removeAttribute('aria-busy');
1359                announce('Intake year options loaded.');
1360            }).catch(function (e) {
1361                console.error('Intake year fetch error:', e);
1362                sel.innerHTML = '<option value="-1">Could not load intake years</option>';
1363                sel.removeAttribute('aria-busy');
1364            });
1365        }());
1366
1367        (function () {
1368            var schoolSel = document.getElementById('fx_schoolInstitution');
1369            schoolSel.setAttribute('aria-busy', 'true');
1370            fetchSchoolList().then(function (data) {
1371                populateSelect('fx_schoolInstitution', data, 'Select...');
1372                schoolSel.disabled = false;
1373                schoolSel.removeAttribute('aria-busy');
1374                if (typeof $ !== 'undefined' && $.fn.select2) {
1375                    $('#fx_schoolInstitution').select2({ width: '100%', minimumInputLength: 3 });
1376                }
1377            }).catch(function (e) {
1378                console.error('School list error:', e);
1379                schoolSel.innerHTML = '<option value="">Could not load school list</option>';
1380                schoolSel.removeAttribute('aria-busy');
1381            });
1382        }());
1383    });
1384
1385})();
1386
1387document.addEventListener("DOMContentLoaded", function () {
1388
1389    const closeBtn = document.querySelector(".alert-close");
1390    const message = document.getElementById("nsfas-message");
1391
1392    if (closeBtn && message) {
1393
1394        closeBtn.addEventListener("click", function () {
1395
1396            message.style.display = "none";
1397
1398        });
1399
1400    }
1401
1402});

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.