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 + '®Id=' + 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.