1const select = document.getElementsByName("areasofstudy_dd")[0]; 2 3let previousSelected = []; 4select.addEventListener("change", function () { 5 const allOptions = Array.from(this.options); 6 let selectedOptions = Array.from(this.selectedOptions); 7 const currentValues = selectedOptions.map(o => o.value); 8 const newlySelected = currentValues.find(v => !previousSelected.includes(v)); 9 10 // Prevent selecting more than 3 11 if (selectedOptions.length > 3 && newlySelected) { 12 const optionToUnselect = allOptions.find(o => o.value === newlySelected); 13 if (optionToUnselect) optionToUnselect.selected = false; 14 selectedOptions = Array.from(this.selectedOptions); 15 } 16 17 // Limit to 3 selections 18 if (selectedOptions.length >= 3) { 19 allOptions.forEach(option => { 20 if (!option.selected && option.value !== "") { 21 option.disabled = true; 22 } 23 }); 24 } else { 25 allOptions.forEach(option => option.disabled = false); 26 } 27 28 // Save current state 29 previousSelected = selectedOptions.map(o => o.value); 30 31 // Get display text 32 const values = selectedOptions 33 .map(option => option.text) 34 .filter(v => v !== ""); 35 36 const inputs = [ 37 document.getElementsByName("selection1")[0], 38 document.getElementsByName("selection2")[0], 39 document.getElementsByName("selection3")[0] 40 ]; 41 42 const fields = inputs.map(input => ({ 43 input, 44 container: input ? input.closest('.cmp-adaptiveform-textinput') : null 45 })); 46 47 fields.forEach((field, index) => { 48 if (!field.input || !field.container) return; 49 50 const value = values[index] || ""; 51 52 // Set value 53 field.input.value = value; 54 55 // Show/Hide 56 field.container.style.display = value ? "block" : "none"; 57 58 // AEM sync 59 field.container.setAttribute("data-cmp-visible", value ? "true" : "false"); 60 61 field.input.dispatchEvent(new Event("change", { bubbles: true })); 62 }); 63 64 // Update summary (count only) 65 const summaryDiv = document.getElementById("text-d875f1687c"); 66 if (summaryDiv && summaryDiv.querySelector("p")) { 67 summaryDiv.querySelector("p").textContent = 68 `Your Selection Summary (${values.length})`; 69 } 70 71 console.log("Selected:", values.join(", ")); 72}); 73 74 75select.dispatchEvent(new Event("change")); 76 77const firstNameInput = document.getElementsByName("firstName")[0]; 78const targetInput = document.getElementsByName("displayOpenday")[0]; 79 80 81function formatName(name) { 82 return name.endsWith("s") ? `${name}' Open Day` : `${name}'s Open Day`; 83} 84 85 firstNameInput.addEventListener("input", function (e) { 86 const firstName = e.target.value.trim(); 87 88 const result = firstName ? formatName(firstName) : ""; 89 90 console.log("Updating field:", result); 91 92 targetInput.value = result; 93 }); 94 95/** 96 * Collects filled guest name/email pairs, updates the guestList summary field, 97 * and sets numberOfGuests to the valid guest count plus 1 for the primary registrant. 98 * @name updateGuestSummary 99 * @return {void} 100 */ 101function updateGuestSummary() { 102 const nameFields = document.getElementsByName("guestName"); 103 const emailFields = document.getElementsByName("guestEmail"); 104 const targetField = document.getElementsByName("guestList")[0]; 105 106 if (!targetField) return; 107 108 let result = []; 109 let hasValidGuest = false; 110 for (let i = 0; i < nameFields.length; i++) { 111 const name = nameFields[i].value.trim(); 112 const email = emailFields[i]?.value.trim(); 113 114 if (name && email) { 115 hasValidGuest = true; 116 result.push(`${name} ${email}`); 117 } 118 } 119 120 targetField.value = result.join("\n"); 121 const container = targetField.closest(".cmp-adaptiveform-textinput"); 122 if (container) { 123 container.style.display = hasValidGuest ? "block" : "none"; 124 } 125 const infoText = document.getElementById("text-e4a199b6fa"); 126 if (infoText) { 127 infoText.style.display = hasValidGuest ? "block" : "none"; 128 } 129} 130 131function getSelectedSessionsData() { 132 const data = localStorage.getItem("selectedSessions"); 133 134 if (!data) { 135 return null; 136 } 137 138 try { 139 // If it's already a stringified JSON, parse & re-stringify (safe normalization) 140 const parsed = JSON.parse(data); 141 return JSON.stringify(parsed); 142 } catch (e) {
143 // If it's not valid JSON, just return as-is 144 return data; 145 } 146} 147 148/** 149 * Reloads the current page. 150 * @name refreshPage 151 * @return {void} 152 */ 153function refreshPage() { 154 location.reload(); 155} 156 157/** 158 * Sends guest invite emails via the AJO unitary execute endpoint for all valid guests. 159 * @name submitGuestInvites 160 * @param {string} primaryEmail - Email address of the primary registrant. 161 * @param {string} primaryName - First name of the primary registrant. 162 * @return {void} 163 */ 164async function submitGuestInvites(primaryEmail, primaryName) { 165 const nameFields = document.getElementsByName("guestName"); 166 const emailFields = document.getElementsByName("guestEmail"); 167 168 let recipients = []; 169 170 for (let i = 0; i < nameFields.length; i++) { 171 const name = nameFields[i].value.trim(); 172 const email = emailFields[i]?.value.trim(); 173 174 if (name && email) { 175 recipients.push({ 176 emailAddress: email, 177 firstName: name 178 }); 179 } 180 } 181 182 if (recipients.length === 0) { 183 console.warn("No valid guests found."); 184 return false; 185 } 186 187 try { 188 for (const guest of recipients) { 189 190 // Get signature per guest 191 const sigResponse = await fetch("/bin/wsu/ajo/signature.json", { 192 method: "POST", 193 headers: { "Content-Type": "application/json" }, 194 credentials: "include", 195 body: JSON.stringify({ email: primaryEmail }) 196 }); 197 198 const sigData = await sigResponse.json(); 199 200 if (!sigData?.requestSignature || !sigData?.requestTimestamp) { 201 console.error("Signature failed for:", guest.emailAddress); 202 continue; 203 } 204 205 // Execute call 206 const execResponse = await fetch("/bin/wsu/ajo/unitary/execute.json", { 207 method: "POST", 208 headers: { 209 "Content-Type": "application/json", 210 "x-ajo-origin": window.location.host 211 }, 212 credentials: "include", 213 body: JSON.stringify({ 214 primaryUser: { 215 emailAddress: primaryEmail, 216 firstName: primaryName 217 }, 218 recipients: [guest], 219 requestTimestamp: sigData.requestTimestamp, 220 requestSignature: sigData.requestSignature 221 }) 222 }); 223 224 const data = await execResponse.json(); 225 console.log("Sent to:", guest.emailAddress, data); 226 } 227 228 return true; 229 230 } catch (error) { 231 console.error("Error sending invites:", error); 232 return false; 233 } 234} 235 236/** 237 * Returns the total number of attendees (valid guests + 1 for the primary registrant). 238 * Use in a form rule: Set Value of numberOfGuests to getGuestCount() 239 * @name getGuestCount 240 * @return {string} Total attendee count as a string. 241 */ 242function getGuestCount() { 243 var count = 0; 244 document.querySelectorAll('.guest-details-panel').forEach(function (panel) { 245 var nameInput = panel.querySelector('input[name="guestName"]'); 246 var emailInput = panel.querySelector('input[name="guestEmail"]'); 247 var nameContainer = nameInput && nameInput.closest('.cmp-adaptiveform-textinput'); 248 var emailContainer = emailInput && emailInput.closest('.cmp-adaptiveform-textinput'); 249 if (nameContainer && nameContainer.classList.contains('cmp-adaptiveform-textinput--filled') && 250 emailContainer && emailContainer.classList.contains('cmp-adaptiveform-textinput--filled')) { 251 count++; 252 } 253 }); 254 return String(count + 1); 255} 256 257/** 258 * Validates all guest email fields for format, duplicates, and match against the primary email. 259 * Also sets the numberOfGuests field value via the AEM Forms rule engine globals. 260 * @name validateGuestEmails 261 * @param {string} primaryEmail - The primary registrant's email to check against. 262 * @param {scope} globals - AEM Forms globals injected by the rule engine. 263 * @return {boolean} True if all guest emails are valid, false otherwise. 264 */ 265function validateGuestEmails(primaryEmail) { 266 const emailFields = Array.from(document.getElementsByName("guestEmail")); 267 const nameFields = Array.from(document.getElementsByName("guestName")); 268 269 const normalizedPrimary = primaryEmail?.trim().toLowerCase(); 270 const seen = new Set(); 271 let isValid = true; 272 273 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; 274 275 emailFields.forEach((field, index) => { 276 const rawValue = field.value.trim(); 277 const email = rawValue.toLowerCase(); 278 const guestName = nameFields[index]?.value.trim(); 279 280 const container = field.closest(".cmp-adaptiveform-textinput"); 281 const errorContainer = container 282 ? container.querySelector(".cmp-adaptiveform-textinput__errormessage") 283 : null; 284 285 let message = ""; 286 287 // Email is empty 288 if (!rawValue) { 289 if (guestName) { 290 message = "This field is required."; 291 } 292 // else no validation 293 } 294 295 // Format validation 296 else if (!emailRegex.test(email)) { 297 message = "Invalid email format."; 298 } 299 300 // Custom rules 301 else { 302 if (seen.has(email)) { 303 message = "Duplicate guest email not allowed."; 304 } else { 305 seen.add(email); 306 } 307 308 if (normalizedPrimary && email === normalizedPrimary) { 309 message += (message ? " " : "") + "Cannot use primary email as guest."; 310 } 311 } 312 313 // UI handling 314 if (message) { 315 isValid = false;
316 317 if (errorContainer) { 318 errorContainer.textContent = message; 319 errorContainer.style.display = "block"; 320 } 321 322 field.setAttribute("aria-invalid", "true"); 323 324 } else { 325 if (errorContainer) { 326 errorContainer.textContent = ""; 327 errorContainer.style.display = "none"; 328 } 329 330 field.setAttribute("aria-invalid", "false"); 331 } 332 }); 333 334 return isValid; 335} 336/** 337 * Returns the pathname of the referring page, or empty string if no referrer. 338 * @name captureSourcePath 339 * @return {string} The source pathname. 340 */ 341function captureSourcePath() { 342 var referrer = document.referrer; 343 var sourcePath = ""; 344 if (referrer) { 345 try { 346 var url = new URL(referrer); 347 sourcePath = url.pathname; 348 } catch (e) { 349 sourcePath = referrer; 350 } 351 } 352 return sourcePath; 353} 354 355/** 356 * Full international mobile number for a phone field: its data-country-code 357 * (set by the calling-code picker) + the national digits, with no leading "+" 358 * and a single leading trunk "0" dropped. Idempotent; does not alter the field. 359 * Intended for a form rule that populates a hidden field. 360 * 361 * @param {string} fieldName Element name of the phone field, e.g. "phoneNumber". 362 * @return {string} e.g. "61412345678" (empty if the field has no digits). 363 */ 364function getInternationalPhoneNumber(fieldName) { 365 var input = document.querySelector('[name="' + fieldName + '"]'); 366 if (!input) { 367 return ""; 368 } 369 var code = input.getAttribute("data-country-code") || ""; 370 var codeDigits = code.replace(/\D/g, ""); 371 var digits = String(input.value || "").replace(/\D/g, ""); 372 if (!digits) { 373 return ""; 374 } 375 // If the value is already prefixed with the code, don't add it again. 376 var national = (codeDigits && digits.indexOf(codeDigits) === 0 && 377 digits.length > codeDigits.length) 378 ? digits.slice(codeDigits.length) 379 : digits; 380 national = national.replace(/^0/, ""); 381 return codeDigits + national; 382} 383function _forms_escapeHtml(text) { 384 if (text == null || text === '') { 385 return ''; 386 } 387 return String(text).replace(/[&<>"']/g, function (c) { 388 return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; 389 }); 390} 391 392/** Attribute values in quoted HTML; same character set as text escape for this project. */ 393function _forms_escapeAttr(text) { 394 return _forms_escapeHtml(text); 395} 396 397function _forms_normalizeAreaOfStudy(areaOfStudy) { 398 if (Array.isArray(areaOfStudy)) { 399 return areaOfStudy; 400 } 401 if (typeof areaOfStudy === 'string') { 402 var s = areaOfStudy.trim(); 403 if (s.charAt(0) === '[') { 404 try { 405 var parsed = JSON.parse(s); 406 return Array.isArray(parsed) ? parsed : []; 407 } catch (e) { 408 return s ? [s] : []; 409 } 410 } 411 if (s.indexOf(',') !== -1) { 412 return s.split(',').map(function (x) { 413 return x.trim(); 414 }).filter(Boolean); 415 } 416 return s ? [s] : []; 417 } 418 return []; 419} 420 421function _forms_buildOpendaySessionsGraphqlPath(areaOfStudy, studyOption) { 422 var labels = ['areaA', 'areaB', 'areaC']; 423 var raw = _forms_normalizeAreaOfStudy(areaOfStudy); 424 var segment = 'fetchOpendaySessions'; 425 var i; 426 for (i = 0; i < 3; i++) { 427 var v = raw[i]; 428 var val = 429 v !== undefined && v !== null && String(v).trim() !== '' 430 ? String(v).trim() 431 : 'null'; 432 segment += ';' + labels[i] + '=' + encodeURIComponent(val); 433 } 434 segment += ';studyOption=' + encodeURIComponent(studyOption || ''); 435 return segment.replace(/;/g, '%3B'); 436} 437 438function _forms_getOrigin() { 439 try { 440 if (typeof window !== 'undefined' && window.location && window.location.origin) { 441 return window.location.origin; 442 } 443 } catch (e) { 444 /* ignore */ 445 } 446 return ''; 447} 448 449/** 450 * Root-relative media URLs: prefix on Author (current origin); on publish, leave as-is. 451 */ 452function _forms_needsPublishDeliveryBaseForMedia() { 453 try { 454 var h = window.location.hostname; 455 return h.indexOf('author') !== -1; 456 } catch (e) { 457 return false; 458 } 459} 460 461/** 462 * Prefix root-relative _dynamicUrl / _path when needed on Author (window.location.origin). 463 * _publishUrl is already absolute and is not modified in _forms_sessionImageUrl. 464 */ 465function _forms_absolutePublishImageUrlIfLocal(url, sourceKind) { 466 if (!url) { 467 return ''; 468 } 469 var s = String(url).trim(); 470 if (!_forms_needsPublishDeliveryBaseForMedia() || (sourceKind !== 'dynamic' && sourceKind !== 'path')) { 471 return s; 472 } 473 if (/^https?:\/\//i.test(s)) { 474 return s; 475 } 476 var base = _forms_getOrigin().replace(/\/+$/, ''); 477 if (!base) { 478 return s; 479 } 480 var path = s.charAt(0) === '/' ? s : '/' + s; 481 return base + path; 482} 483 484/** 485 * Rich text from GraphQL (e.g. session.summary.html); empty if missing. 486 */ 487function _forms_sessionSummaryHtml(s) { 488 if (!s || !s.summary) { 489 return ''; 490 } 491 var html = s.summary.html; 492 if (typeof html !== 'string') { 493 return ''; 494 } 495 return html.trim(); 496} 497 498/** 499 * Plain text from rich summary HTML (for localStorage; no tags). 500 */ 501function _forms_htmlToPlainText(html) { 502 if (html == null || html === '') { 503 return ''; 504 } 505 var s = String(html); 506 try { 507 if (typeof document !== 'undefined') { 508 var d = document.createElement('div'); 509 d.innerHTML = s; 510 var t = d.textContent || d.innerText || ''; 511 return String(t).replace(/\s+/g, ' ').trim(); 512 } 513 } catch (e) { 514 /* fall through */ 515 } 516 return s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); 517} 518 519function _forms_sessionImageUrl(session) { 520 if (!session || !session.image) { 521 return ''; 522 } 523 var im = session.image; 524 if (im._dynamicUrl) { 525 return _forms_absolutePublishImageUrlIfLocal(im._dynamicUrl, 'dynamic'); 526 } 527 if (im._publishUrl) { 528 return im._publishUrl; 529 } 530 if (im._path) { 531 return _forms_absolutePublishImageUrlIfLocal(im._path, 'path'); 532 } 533 return ''; 534} 535 536function _forms_formatSessionTime(iso) { 537 if (!iso) { 538 return ''; 539 } 540 var d = new Date(iso); 541 if (isNaN(d.getTime())) { 542 return ''; 543 } 544 return d.toLocaleTimeString(undefined, { 545 hour: 'numeric', 546 minute: '2-digit', 547 hour12: true 548 }); 549} 550 551/** 552 * Groups sessions by clock time only (HH:MM:SS); date and zone are ignored.
553 * Accepts common ISO-8601 time forms, e.g. with or without seconds, fractional seconds. 554 */ 555function _forms_startTimeGroupKey(iso) { 556 if (!iso || typeof iso !== 'string') { 557 return ''; 558 } 559 var m = iso.match(/T(\d{2}:\d{2})(?::(\d{2}))?/); 560 if (!m) { 561 return ''; 562 } 563 var sec = m[2] != null && m[2] !== '' ? m[2] : '00'; 564 return m[1] + ':' + sec; 565} 566 567/** Milliseconds for ascending sort; missing/invalid ISO sorts last. */ 568function _forms_startDateTimeToSortMs(iso) { 569 if (iso == null || iso === '') { 570 return Number.POSITIVE_INFINITY; 571 } 572 var t = new Date(iso).getTime(); 573 return isNaN(t) ? Number.POSITIVE_INFINITY : t; 574} 575 576/** Safe sort comparator for ISO start strings (handles invalid dates and ties). */ 577function _forms_compareStartMsAscending(isoA, isoB) { 578 var ta = _forms_startDateTimeToSortMs(isoA); 579 var tb = _forms_startDateTimeToSortMs(isoB); 580 return (ta > tb) - (ta < tb); 581} 582 583function _forms_minStartMsInSessions(sessions) { 584 var min = Number.POSITIVE_INFINITY; 585 var i; 586 for (i = 0; i < sessions.length; i++) { 587 var m = _forms_startDateTimeToSortMs(sessions[i].startDateTime); 588 if (m < min) { 589 min = m; 590 } 591 } 592 return min; 593} 594 595/** 596 * Priority sessions first (lower numeric priority first); remaining sessions by startDateTime ascending. 597 */ 598function _forms_sortSessionsForTimeSlot(sessions) { 599 var withPri = []; 600 var withoutPri = []; 601 sessions.forEach(function (s) { 602 if (s.priority != null && s.priority !== '') { 603 withPri.push(s); 604 } else { 605 withoutPri.push(s); 606 } 607 }); 608 withPri.sort(function (a, b) { 609 var pa = Number(a.priority); 610 var pb = Number(b.priority); 611 var aNa = isNaN(pa); 612 var bNa = isNaN(pb); 613 if (aNa && bNa) { 614 return 0; 615 } 616 if (aNa) { 617 return 1; 618 } 619 if (bNa) { 620 return -1; 621 } 622 return pa - pb; 623 }); 624 withoutPri.sort(function (a, b) { 625 return _forms_compareStartMsAscending(a.startDateTime, b.startDateTime); 626 }); 627 return withPri.concat(withoutPri); 628} 629 630function _forms_sessionCode(s) { 631 if (!s) { 632 return ''; 633 } 634 if (s.code != null && s.code !== '') { 635 return String(s.code); 636 } 637 if (s.sessionCode != null && s.sessionCode !== '') { 638 return String(s.sessionCode); 639 } 640 return ''; 641} 642 643function _forms_storageString(v) { 644 if (v == null) { 645 return ''; 646 } 647 return String(v); 648} 649 650function _forms_sessionCampus(s) { 651 if (!s || s.campus == null) { 652 return ''; 653 } 654 return typeof s.campus === 'string' ? s.campus : ''; 655} 656 657function _forms_sessionToActivity(s) { 658 var img = _forms_sessionImageUrl(s); 659 var htmlSumm = _forms_sessionSummaryHtml(s); 660 return { 661 code: _forms_sessionCode(s), 662 title: s.title || '', 663 areaOfStudy: _forms_storageString(s.areaOfStudy), 664 studyOption: _forms_storageString(s.studyOption), 665 startDateTime: s.startDateTime || '', 666 duration: _forms_storageString(s.duration), 667 location: s.location || '', 668 campus: _forms_sessionCampus(s), 669 description: _forms_htmlToPlainText(htmlSumm), 670 descriptionHtml: htmlSumm, 671 image: img 672 }; 673} 674 675function _forms_sessionCollapsedMeta(s) { 676 var img = _forms_sessionImageUrl(s); 677 return { 678 image: img, 679 title: s.title || '', 680 descriptionHtml: _forms_sessionSummaryHtml(s), 681 location: s.location || '' 682 }; 683} 684 685/** 686 * Flattens GraphQL execute response (sessionsA/B/C) into activity picker row payload for the open-day picker. 687 * Sessions with the same start time (time of day only) become one row; carousel lists all. 688 * areaOfStudy and studyOption on each session come from the API (SessionFields fragment), not from fetch() params. 689 */ 690function _forms_graphqlPayloadToActivityPickerItems(payload) { 691 if (!payload || !payload.data) { 692 return []; 693 } 694 var d = payload.data; 695 var keys = ['sessionsA', 'sessionsB', 'sessionsC']; 696 var all = []; 697 keys.forEach(function (k) { 698 if (d[k] && Array.isArray(d[k].items)) { 699 d[k].items.forEach(function (item) { 700 all.push(item); 701 }); 702 } 703 }); 704 705 var groups = {}; 706 all.forEach(function (item) { 707 var gk = _forms_startTimeGroupKey(item.startDateTime); 708 if (!gk) { 709 gk = '_unknown'; 710 } 711 if (!groups[gk]) { 712 groups[gk] = []; 713 } 714 groups[gk].push(item); 715 }); 716 717 var groupKeys = Object.keys(groups); 718 var minMsByGroupKey = {}; 719 var gi; 720 for (gi = 0; gi < groupKeys.length; gi++) { 721 var gKey = groupKeys[gi]; 722 minMsByGroupKey[gKey] = _forms_minStartMsInSessions(groups[gKey]); 723 } 724 groupKeys.sort(function (a, b) { 725 var ma = minMsByGroupKey[a]; 726 var mb = minMsByGroupKey[b]; 727 return (ma > mb) - (ma < mb); 728 }); 729 return groupKeys.map(function (gk) { 730 var slot = _forms_sortSessionsForTimeSlot(groups[gk]); 731 var primary = slot[0]; 732 var timeStr = _forms_formatSessionTime(primary.startDateTime); 733 var collapsed = _forms_sessionCollapsedMeta(primary); 734 return { 735 time: timeStr, 736 duration: primary.duration || '', 737 startDateTime: primary.startDateTime || '', 738 image: collapsed.image, 739 title: collapsed.title, 740 descriptionHtml: collapsed.descriptionHtml, 741 location: collapsed.location, 742 expandHeader: timeStr, 743 activities: slot.map(_forms_sessionToActivity) 744 }; 745 }); 746} 747 748var FORMS_SELECTED_SESSIONS_KEY = 'selectedSessions'; 749 750/** Row model for the open-day picker only (modify/remove/carousel). */ 751var _forms_opendayActivityPickerRows = null; 752 753function _forms_getOpenDayActivityPicker() { 754 return document.querySelector('.openday-activity-picker .wsu-activity-picker'); 755} 756 757/** 758 * Shape written to localStorage "selectedSessions" (no HTML in description). 759 */ 760function _forms_sessionToStoragePlain(activity) { 761 if (!activity) { 762 return { 763 code: '', 764 title: '', 765 areaOfStudy: '', 766 studyOption: '', 767 startDateTime: '', 768 duration: '', 769 location: '', 770 campus: '', 771 description: '', 772 image: '' 773 }; 774 } 775 var descPlain = 776 activity.description != null && String(activity.description).trim() !== '' 777 ? String(activity.description).trim() 778 : _forms_htmlToPlainText(activity.descriptionHtml || ''); 779 return { 780 code: _forms_storageString(activity.code), 781 title: _forms_storageString(activity.title), 782 areaOfStudy: _forms_storageString(activity.areaOfStudy), 783 studyOption: _forms_storageString(activity.studyOption), 784 startDateTime: _forms_storageString(activity.startDateTime), 785 duration: _forms_storageString(activity.duration), 786 location: _forms_storageString(activity.location), 787 campus: _forms_storageString(activity.campus), 788 description: descPlain, 789 image: _forms_storageString(activity.image) 790 }; 791} 792 793function _forms_getSelectedSessionsArray() { 794 try { 795 var raw = localStorage.getItem(FORMS_SELECTED_SESSIONS_KEY); 796 if (!raw) { 797 return []; 798 } 799 var parsed = JSON.parse(raw); 800 return Array.isArray(parsed) ? parsed : []; 801 } catch (e) { 802 return []; 803 } 804} 805 806function _forms_setSelectedSessionsArray(arr) { 807 try { 808 localStorage.setItem(FORMS_SELECTED_SESSIONS_KEY, JSON.stringify(arr)); 809 } catch (e) { 810 /* quota / private mode */ 811 } 812} 813 814function _forms_clearSelectedSessionsOnLoad() { 815 try { 816 if (typeof localStorage !== 'undefined' && localStorage.getItem(FORMS_SELECTED_SESSIONS_KEY) !== null) { 817 localStorage.removeItem(FORMS_SELECTED_SESSIONS_KEY); 818 } 819 } catch (e) { 820 /* quota / private mode */ 821 } 822} 823 824function _forms_cloneActivityPickerRow(row) { 825 return { 826 time: row.time, 827 duration: row.duration, 828 startDateTime: row.startDateTime, 829 image: row.image, 830 title: row.title, 831 descriptionHtml: row.descriptionHtml, 832 location: row.location, 833 expandHeader: row.expandHeader, 834 startTime: row.startTime, 835 activities: Array.isArray(row.activities) ? row.activities.slice() : [] 836 }; 837} 838 839function _forms_findActivityIndexInRow(activities, sel) { 840 if (!sel || !activities || !activities.length) { 841 return 0; 842 } 843 var i; 844 var selCode = _forms_storageString(sel.code); 845 if (selCode !== '') { 846 for (i = 0; i < activities.length; i++) { 847 var ac = _forms_storageString(activities[i].code); 848 if (ac !== '' && ac === selCode) { 849 return i; 850 } 851 } 852 } 853 for (i = 0; i < activities.length; i++) { 854 var a = activities[i]; 855 if ((a.title || '') === (sel.title || '') && (a.location || '')
855=== (sel.location || '')) { 856 return i; 857 } 858 } 859 return 0; 860} 861 862function _forms_selectedActivityIndexForRow(activities, preselectedSessions, itemsLength, rowIndex) { 863 if (preselectedSessions && preselectedSessions.length === itemsLength && preselectedSessions[rowIndex]) { 864 return _forms_findActivityIndexInRow(activities, preselectedSessions[rowIndex]); 865 } 866 return 0; 867} 868 869/** 870 * One localStorage plain session → one picker row (single activity) for summary / final screen. 871 */ 872function _forms_plainSessionToActivityPickerRow(plain) { 873 if (!plain || typeof plain !== 'object') { 874 plain = {}; 875 } 876 var timeStr = _forms_formatSessionTime(plain.startDateTime); 877 var descPlain = _forms_storageString(plain.description).trim(); 878 var imageRaw = _forms_storageString(plain.image); 879 var act = { 880 code: _forms_storageString(plain.code), 881 title: _forms_storageString(plain.title), 882 areaOfStudy: _forms_storageString(plain.areaOfStudy), 883 studyOption: _forms_storageString(plain.studyOption), 884 startDateTime: _forms_storageString(plain.startDateTime), 885 duration: _forms_storageString(plain.duration), 886 location: _forms_storageString(plain.location), 887 campus: _forms_storageString(plain.campus), 888 description: descPlain, 889 descriptionHtml: '', 890 image: _forms_absolutePublishImageUrlIfLocal(imageRaw, 'path') 891 }; 892 return { 893 time: timeStr, 894 duration: act.duration, 895 startDateTime: act.startDateTime, 896 image: act.image, 897 title: act.title, 898 description: descPlain, 899 descriptionHtml: '', 900 location: act.location, 901 expandHeader: timeStr, 902 activities: [act] 903 }; 904} 905 906function _forms_getSelectedSessionsAligned() { 907 var rows = _forms_opendayActivityPickerRows; 908 if (!rows || !rows.length) { 909 return []; 910 } 911 var n = rows.length; 912 var arr = _forms_getSelectedSessionsArray(); 913 if (arr.length !== n) { 914 arr = rows.map(function (row) { 915 var acts = row.activities || []; 916 return acts.length ? _forms_sessionToStoragePlain(acts[0]) : _forms_sessionToStoragePlain({}); 917 }); 918 _forms_setSelectedSessionsArray(arr); 919 } 920 return arr; 921} 922 923function _forms_updateSelectedSessionAtSlot(slotIdx, plain) { 924 var arr = _forms_getSelectedSessionsAligned(); 925 if (slotIdx >= 0 && slotIdx < arr.length) { 926 arr[slotIdx] = plain; 927 _forms_setSelectedSessionsArray(arr); 928 } 929} 930 931var _forms_activityPickerDocCloseBound = false; 932 933function _forms_bindActivityPickerDocumentClose() { 934 if (_forms_activityPickerDocCloseBound) { 935 return; 936 } 937 _forms_activityPickerDocCloseBound = true; 938 document.addEventListener('click', function () { 939 var expanded = document.querySelectorAll('.wsu-activity-picker .activity-item.expanded'); 940 if (!expanded.length) { 941 return; 942 } 943 expanded.forEach(function (el) { 944 el.classList.remove('expanded'); 945 }); 946 }); 947} 948 949/** 950 * When approval checkbox input's value is the string "true", programmatically click it (e.g. after Modify/Remove). 951 */ 952function _forms_clickApprovalCheckboxIfValueTrue() { 953 var approvalInput = document.querySelector('input[name*="approvalcheckbox"]'); 954 if (approvalInput && approvalInput.value === 'true') { 955 approvalInput.click(); 956 } 957} 958 959function _forms_syncActivityCarouselAriaPressed(carousel) { 960 if (!carousel || !carousel.querySelectorAll) { 961 return; 962 } 963 carousel.querySelectorAll('.activity-carousel-item').forEach(function (a) { 964 a.setAttribute('aria-pressed', a.classList.contains('active') ? 'true' : 'false'); 965 }); 966} 967 968/** 969 * Applies a carousel choice: updates summary, localStorage, aria state, and collapses the row. 970 */ 971function _forms_selectActivityCarouselItem(activityItem, activityEl, carousel) { 972 var carouselItems = carousel.querySelectorAll('.activity-carousel-item'); 973 carouselItems.forEach(function (a) { 974 a.classList.remove('active'); 975 }); 976 activityEl.classList.add('active'); 977 _forms_syncActivityCarouselAriaPressed(carousel); 978 979 var collapsedActivityTitleEl = activityItem.querySelector( 980 '.activity-collapsed .activity-collapsed-content .activity-title' 981 ); 982 var collapsedActivityDescEl = activityItem.querySelector( 983 '.activity-collapsed .activity-collapsed-content .activity-summary' 984 ); 985 var collapsedActivityImgEl = activityItem.querySelector('.activity-collapsed > img'); 986 var collapsedLocationEl = activityItem.querySelector('.activity-collapsed .activity-location'); 987 var activityItemTitleEl = activityEl.querySelector('.activity-title'); 988 var activityItemDescEl = activityEl.querySelector('.activity-description'); 989 var activityItemImgEl = activityEl.querySelector('img'); 990 var loc = activityEl.getAttribute('data-location') || ''; 991 992 if (collapsedActivityTitleEl && activityItemTitleEl) { 993 collapsedActivityTitleEl.textContent = activityItemTitleEl.textContent; 994 } 995 if (collapsedActivityDescEl && activityItemDescEl) { 996 collapsedActivityDescEl.innerHTML = activityItemDescEl.innerHTML; 997 } 998 if (collapsedActivityImgEl && activityItemImgEl) { 999 collapsedActivityImgEl.src = activityItemImgEl.src; 1000 collapsedActivityImgEl.alt = activityItemImgEl.getAttribute('alt') || ''; 1001 } 1002 if (collapsedLocationEl) { 1003 var collapsedLocationTextEl = collapsedLocationEl.querySelector('.activity-location-text'); 1004 if (collapsedLocationTextEl) { 1005 collapsedLocationTextEl.textContent = loc; 1006 } else { 1007 collapsedLocationEl.textContent = ''; 1008 var locIconEl = document.createElement('div'); 1009 locIconEl.className = 'activity-location-icon'; 1010 var locTextEl = document.createElement('div'); 1011 locTextEl.className = 'activity-location-text'; 1012 locTextEl.textContent = loc; 1013 collapsedLocationEl.appendChild(locIconEl); 1014 collapsedLocationEl.appendChild(locTextEl); 1015 } 1016 } 1017 1018 var slotIdx = parseInt(activityItem.getAttribute('data-activity-slot-index'), 10); 1019 var rowsPick = _forms_opendayActivityPickerRows; 1020 if (!isNaN(slotIdx) && rowsPick && rowsPick[slotIdx]) { 1021 var actIndex = Array.prototype.indexOf.call(carouselItems, activityEl); 1022 var act = 1023 actIndex >= 0 && rowsPick[slotIdx].activities 1024 ? rowsPick[slotIdx].activities[actIndex] 1025 : null; 1026 if (act) { 1027 _forms_updateSelectedSessionAtSlot(slotIdx, _forms_sessionToStoragePlain(act)); 1028 } 1029 } 1030 1031 activityItem.classList.remove('expanded'); 1032} 1033 1034/** 1035 * Modify visibility: root must allow it (data-modify); each row must have more than one .activity-carousel-item 1036 * under .activity-carousel. Strips Modify when the parsed DOM has at most one carousel item for that row. 1037 */ 1038function _forms_applyActivityPickerModifyVisibility(container) { 1039 if (!container || !container.querySelectorAll) { 1040 return; 1041 } 1042 var modifyAllowed = container.getAttribute('data-modify') === 'true'; 1043 container.querySelectorAll('.activity-item').forEach(function (activityItem) { 1044 var carousel = activityItem.querySelector('.activity-carousel'); 1045 if (!carousel) { 1046 return; 1047 } 1048 var itemCount = carousel.querySelectorAll('.activity-carousel-item').length; 1049 var modifyBtn = activityItem.querySelector('.activity-modify'); 1050 if (!modifyBtn) { 1051 return; 1052 } 1053 if (!modifyAllowed || itemCount <= 1) { 1054 var actions = modifyBtn.parentNode; 1055 if (modifyBtn.parentNode) { 1056 modifyBtn.parentNode.removeChild(modifyBtn); 1057 } 1058 if ( 1059 actions && 1060 actions.classList && 1061 actions.classList.contains('activity-actions') && 1062 !actions.querySelector('button') 1063 ) { 1064 actions.parentNode.removeChild(actions); 1065 } 1066 } 1067 }); 1068} 1069 1070/** 1071 * Expand on Modify tap, close on outside click / activity select;
1071 carousel updates collapsed summary. 1072 * Binds only the open-day root (.openday-activity-picker). 1073 */ 1074function _forms_bindOpenDayActivityPickerInteractions() { 1075 var container = _forms_getOpenDayActivityPicker(); 1076 if (!container) { 1077 return; 1078 } 1079 _forms_bindActivityPickerDocumentClose(); 1080 1081 container.querySelectorAll('.activity-item').forEach(function (activityItem) { 1082 var activityCollapsedEl = activityItem.querySelector('.activity-collapsed'); 1083 if (!activityCollapsedEl) { 1084 return; 1085 } 1086 1087 var modifyBtn = activityCollapsedEl.querySelector('.activity-modify'); 1088 if (modifyBtn) { 1089 modifyBtn.addEventListener('click', function (e) { 1090 e.stopPropagation(); 1091 _forms_clickApprovalCheckboxIfValueTrue(); 1092 activityItem.classList.add('expanded'); 1093 var expandCarousel = activityItem.querySelector('.activity-carousel'); 1094 if (expandCarousel) { 1095 var focusTarget = 1096 expandCarousel.querySelector('.activity-carousel-item.active') || 1097 expandCarousel.querySelector('.activity-carousel-item'); 1098 if (focusTarget && typeof focusTarget.focus === 'function') { 1099 focusTarget.focus(); 1100 } 1101 } 1102 }); 1103 } 1104 1105 var removeBtn = activityCollapsedEl.querySelector('.activity-remove'); 1106 if (removeBtn) { 1107 removeBtn.addEventListener('click', function (e) { 1108 e.stopPropagation(); 1109 _forms_clickApprovalCheckboxIfValueTrue(); 1110 var slotIdx = parseInt(activityItem.getAttribute('data-activity-slot-index'), 10); 1111 var rows = _forms_opendayActivityPickerRows; 1112 if (isNaN(slotIdx) || !rows) { 1113 return; 1114 } 1115 if (slotIdx < 0 || slotIdx >= rows.length) { 1116 return; 1117 } 1118 var arr = _forms_getSelectedSessionsAligned(); 1119 rows.splice(slotIdx, 1); 1120 arr.splice(slotIdx, 1); 1121 _forms_renderOpenDayActivityPickerItems(rows, arr); 1122 }); 1123 } 1124 1125 activityItem.addEventListener('click', function (e) { 1126 e.stopPropagation(); 1127 }); 1128 1129 var carousel = activityItem.querySelector('.activity-carousel'); 1130 if (!carousel) { 1131 return; 1132 } 1133 1134 carousel.querySelectorAll('.activity-carousel-item').forEach(function (activityEl) { 1135 activityEl.addEventListener('click', function (e) { 1136 e.stopPropagation(); 1137 _forms_selectActivityCarouselItem(activityItem, activityEl, carousel); 1138 }); 1139 activityEl.addEventListener('keydown', function (e) { 1140 var key = e.key; 1141 var code = e.keyCode; 1142 var activate = 1143 key === 'Enter' || 1144 key === ' ' || 1145 key === 'Spacebar' || 1146 (!key && (code === 13 || code === 32)); 1147 if (!activate) { 1148 return; 1149 } 1150 if (key === ' ' || key === 'Spacebar' || code === 32) { 1151 e.preventDefault(); 1152 } 1153 e.stopPropagation(); 1154 _forms_selectActivityCarouselItem(activityItem, activityEl, carousel); 1155 }); 1156 }); 1157 }); 1158} 1159 1160function _forms_compareActivityPickerRowsByTime(a, b) { 1161 return _forms_compareStartMsAscending(a && a.startDateTime, b && b.startDateTime); 1162} 1163 1164/** 1165 * @param {'openDay'|'confirm'} mode openDay: Remove uses data-remove. Modify only when data-modify is true and 1166 * each row's .activity-carousel has more than one .activity-carousel-item (enforced on the parsed DOM after render). 1167 * confirm: always read-only, no localStorage writes 1168 */ 1169function _forms_renderActivityPickerIntoElement(container, data, preselectedSessions, mode) { 1170 if (!container || !container.classList || !container.classList.contains('wsu-activity-picker')) { 1171 return; 1172 } 1173 1174 var forceReadonly = mode === 'confirm'; 1175 var modifyEnabledOnRoot = !forceReadonly && container.getAttribute('data-modify') === 'true'; 1176 var showRemoveBtn = !forceReadonly && container.getAttribute('data-remove') === 'true'; 1177 var isReadonlyPicker = forceReadonly || (!modifyEnabledOnRoot && !showRemoveBtn); 1178 1179 var items = Array.isArray(data) ? data : []; 1180 if (mode === 'openDay') { 1181 if (isReadonlyPicker) { 1182 _forms_opendayActivityPickerRows = null; 1183 } else { 1184 _forms_opendayActivityPickerRows = items.map(_forms_cloneActivityPickerRow); 1185 } 1186 } 1187 1188 var html = items.map(function (item, index) { 1189 var time = _forms_escapeHtml(item.time || item.startTime || ''); 1190 var duration = _forms_escapeHtml(item.duration || ''); 1191 var expandHeader = _forms_escapeHtml( 1192 item.expandHeader || item.expandTime || item.time || item.startTime || '' 1193 ); 1194 1195 var activities = Array.isArray(item.activities) ? item.activities : []; 1196 var showModifyBtn = modifyEnabledOnRoot && activities.length > 1; 1197 var actionButtons = ''; 1198 if (showModifyBtn) { 1199 actionButtons += '\n <button type="button" class="activity-modify">Modify</button>'; 1200 } 1201 if (showRemoveBtn) { 1202 actionButtons += '\n <button type="button" class="activity-remove">Remove</button>'; 1203 } 1204 var actionsBlock = 1205 actionButtons !== '' 1206 ? '\n <div class="activity-actions">' + actionButtons + '\n </div>' 1207 : ''; 1208 1209 var selectedIdx = _forms_selectedActivityIndexForRow( 1210 activities, 1211 preselectedSessions, 1212 items.length, 1213 index 1214 ); 1215 var collapsedAct = activities[selectedIdx] || {}; 1216 var img = _forms_escapeAttr( 1217 collapsedAct.image || item.image || item.selectedImage || item.imgSrc || '' 1218 ); 1219 var title = _forms_escapeHtml(collapsedAct.title || item.title || ''); 1220 var collapsedAlt = _forms_escapeAttr( 1221 String(collapsedAct.title || item.title || '').trim() 1222 ); 1223 var desc = 1224 collapsedAct.descriptionHtml && String(collapsedAct.descriptionHtml).trim() !== '' 1225 ? collapsedAct.descriptionHtml 1226 : item.descriptionHtml && String(item.descriptionHtml).trim() !== '' 1227 ? item.descriptionHtml 1228 : _forms_escapeHtml( 1229 _forms_storageString(collapsedAct.description).trim() || 1230 _forms_storageString(item.description).trim() 1231 ); 1232 var location = _forms_escapeHtml(collapsedAct.location || item.location || ''); 1233 1234 var activitiesHtml = activities.map(function (act, actIndex) { 1235 var activeClass = actIndex === selectedIdx ? ' active' : ''; 1236 var loc = _forms_escapeAttr(act.location || act.dataLocation || '');
1237 var aImg = _forms_escapeAttr(act.image || act.imgSrc || ''); 1238 var aTitle = _forms_escapeHtml(act.title || ''); 1239 var aAlt = _forms_escapeAttr(String(act.title || '').trim()); 1240 var aDesc = 1241 act.descriptionHtml && String(act.descriptionHtml).trim() !== '' 1242 ? act.descriptionHtml 1243 : _forms_escapeHtml(_forms_storageString(act.description).trim()); 1244 var a11yAttrs = ''; 1245 if (!isReadonlyPicker) { 1246 var titlePlain = String(act.title || '').trim(); 1247 var pickLabel = 1248 titlePlain !== '' ? 'Select session: ' + titlePlain : 'Select this session'; 1249 a11yAttrs = 1250 ' tabindex="0" role="button" aria-pressed="' + 1251 (actIndex === selectedIdx ? 'true' : 'false') + 1252 '" aria-label="' + 1253 _forms_escapeAttr(pickLabel) + 1254 '"'; 1255 } 1256 return ( 1257 ' <div class="activity-carousel-item' + 1258 activeClass + 1259 '"' + 1260 a11yAttrs + 1261 ' data-location="' + 1262 loc + 1263 '">' + 1264 '\n <img src="' + aImg + '" alt="' + aAlt + '" />' + 1265 '\n <p class="activity-title">' + aTitle + '</p>' + 1266 '\n <div class="activity-description">' + aDesc + '</div>' + 1267 '\n </div>' 1268 ); 1269 }).join('\n'); 1270 1271 var expandedBlock = isReadonlyPicker 1272 ? '' 1273 : '\n <div class="activity-expanded">' + 1274 '\n' + 1275 '\n <div class="activity-expanded-header" id="activity-expanded-header-' + 1276 index + 1277 '">' + 1278 expandHeader + 1279 '</div>' + 1280 '\n' + 1281 '\n <div class="activity-carousel-wrapper">' + 1282 '\n <div class="activity-carousel" role="group" aria-labelledby="activity-expanded-header-' + 1283 index + 1284 '">' + 1285 '\n' + 1286 '\n' + activitiesHtml + 1287 '\n </div>' + 1288 '\n </div>' + 1289 '\n' + 1290 '\n </div>'; 1291 1292 return ( 1293 ' <div class="activity-item" id="activityItem-' + index + '" data-activity-slot-index="' + index + '">' + 1294 '\n <div class="activity-item-time">' + 1295 '\n ' + time + 1296 '\n <span>' + duration + '</span>' + 1297 '\n </div>' + 1298 '\n' + 1299 '\n <div class="activity-card">' + 1300 '\n' + 1301 '\n <div class="activity-collapsed" id="activityCollapsed-' + index + '">' + 1302 '\n <img id="selectedImg-' + index + '" src="' + img + '" alt="' + collapsedAlt + '" />' + 1303 '\n' + 1304 '\n <div class="activity-collapsed-content">' + 1305 '\n <p class="activity-title" id="selectedActivityTitle-' + index + '">' + title + '</p>' + 1306 '\n <div class="activity-summary" id="selectedActivitySummary-' + index + '">' + desc + '</div>' + 1307 '\n <div class="activity-location" id="selectedActivityLocation-' + index + '"><div class="activity-location-icon"></div><div class="activity-location-text">' + location + '</div></div>' + 1308 '\n </div>' + 1309 actionsBlock + 1310 '\n </div>' + 1311 '\n' + 1312 expandedBlock + 1313 '\n' + 1314 '\n </div>' + 1315 '\n </div>' 1316 ); 1317 }).join('\n\n'); 1318 1319 container.innerHTML = html; 1320 _forms_applyActivityPickerModifyVisibility(container); 1321 1322 if (!isReadonlyPicker && mode === 'openDay') { 1323 var selectedSessionsForStorage = items.map(function (item, index) { 1324 var activities = Array.isArray(item.activities) ? item.activities : []; 1325 var selectedIdx = _forms_selectedActivityIndexForRow( 1326 activities, 1327 preselectedSessions, 1328 items.length, 1329 index 1330 ); 1331 if (activities[selectedIdx]) { 1332 return _forms_sessionToStoragePlain(activities[selectedIdx]); 1333 } 1334 return _forms_sessionToStoragePlain({}); 1335 }); 1336 _forms_setSelectedSessionsArray(selectedSessionsForStorage); 1337 _forms_bindOpenDayActivityPickerInteractions(); 1338 } 1339} 1340 1341/** GraphQL / open-day root: .wsu-activity-picker.openday-activity-picker */ 1342function _forms_renderOpenDayActivityPickerItems(data, preselectedSessions) { 1343 var root = _forms_getOpenDayActivityPicker(); 1344 if (!root) { 1345 return; 1346 } 1347 _forms_renderActivityPickerIntoElement(root, data, preselectedSessions, 'openDay'); 1348} 1349 1350/**Scroll to top of the page on each step of the form */ 1351function _forms_scrollToTop() { 1352 window.scrollTo({ 1353 top: 0, 1354 behavior: 'smooth' 1355 }); 1356} 1357 1358/** 1359 * Confirmation panel: render .confirm-activity-picker from localStorage key "selectedSessions". 1360 * Expects a JSON array of plain session objects: code, title, areaOfStudy, studyOption, startDateTime, 1361 * duration, location, campus, description, image (root-relative or absolute URLs). 1362 * Call when that panel becomes visible (e.g. adaptive form navigation). 1363 */
1364function renderSelectedSessions() { 1365 _forms_scrollToTop(); 1366 var root = document.querySelector('.confirm-activity-picker .wsu-activity-picker'); 1367 if (!root) { 1368 return; 1369 } 1370 var stored = _forms_getSelectedSessionsArray(); 1371 var ordered = 1372 stored.length > 1 ? stored.slice().sort(_forms_compareActivityPickerRowsByTime) : stored; 1373 var rows = ordered.map(_forms_plainSessionToActivityPickerRow); 1374 _forms_renderActivityPickerIntoElement(root, rows, ordered, 'confirm'); 1375} 1376 1377function fetchAvailableSessions(areaOfStudy, studyOption) { 1378 _forms_scrollToTop(); 1379 var pathSegment = _forms_buildOpendaySessionsGraphqlPath(areaOfStudy, studyOption); 1380 var base = _forms_getOrigin().replace(/\/+$/, ''); 1381 var url = base + '/graphql/execute.json/global/' + pathSegment; 1382 1383 return fetch(url) 1384 .then(function (response) { 1385 if (!response.ok) { 1386 throw new Error('HTTP ' + response.status + ': ' + (response.statusText || 'request failed')); 1387 } 1388 return response.json(); 1389 }) 1390 .then(function (payload) { 1391 var activityPickerItems = _forms_graphqlPayloadToActivityPickerItems(payload); 1392 var itemsToRender; 1393 if (activityPickerItems.length > 0) { 1394 itemsToRender = activityPickerItems; 1395 } else if (payload && Array.isArray(payload)) { 1396 itemsToRender = 1397 payload.length > 1 1398 ? payload.slice().sort(_forms_compareActivityPickerRowsByTime) 1399 : payload; 1400 } else { 1401 itemsToRender = []; 1402 } 1403 _forms_renderOpenDayActivityPickerItems(itemsToRender, undefined); 1404 }) 1405 .catch(function (err) { 1406 console.error('fetchActivityPickerSessions failed', err); 1407 _forms_renderOpenDayActivityPickerItems([], undefined); 1408 }); 1409 1410} 1411 1412if (typeof document !== 'undefined') { 1413 if (document.readyState === 'loading') { 1414 document.addEventListener('DOMContentLoaded', _forms_clearSelectedSessionsOnLoad); 1415 } else { 1416 _forms_clearSelectedSessionsOnLoad(); 1417 } 1418} 1419 1420 1421(function () { 1422 function summaryHtmlToPlainText(raw) { 1423 if (!raw) { 1424 return ""; 1425 } 1426 var doc = new DOMParser().parseFromString(raw, "text/html"); 1427 var text = doc.body.textContent || ""; 1428 // and other space-like characters → normal space; strip zero-width / BOM 1429 text = text.replace(/[\u00a0\u2007\u202f\u2000-\u2006\u2008-\u200a\u3000]/g, " "); 1430 text = text.replace(/[\u200b\ufeff]/g, ""); 1431 text = text.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); 1432 return text; 1433 } 1434 1435 function sanitizeCardSummaries(scope) { 1436 var nodes = scope.querySelectorAll(".card-carousel__card-summary"); 1437 var n; 1438 var raw; 1439 var plain; 1440 1441 for (n = 0; n < nodes.length; n++) { 1442 if (nodes[n].getAttribute("data-card-summary-sanitized") === "true") { 1443 continue; 1444 } 1445 raw = nodes[n].textContent.replace(/^\s+|\s+$/g, ""); 1446 if (!raw) { 1447 nodes[n].setAttribute("data-card-summary-sanitized", "true"); 1448 continue; 1449 } 1450 1451 plain = summaryHtmlToPlainText(raw); 1452 nodes[n].textContent = plain; 1453 nodes[n].setAttribute("data-card-summary-sanitized", "true"); 1454 } 1455 } 1456 1457 function scrollStep(track) { 1458 var cards = track.querySelectorAll(".card-carousel__card"); 1459 if (cards.length < 2) { 1460 return Math.max(1, Math.round(track.clientWidth * 0.85)); 1461 } 1462 return cards[1].offsetLeft - cards[0].offsetLeft; 1463 } 1464 1465 function updateNav(track, prev, next) { 1466 var maxScroll = track.scrollWidth - track.clientWidth; 1467 var epsilon = 2; 1468 var left = track.scrollLeft; 1469 1470 if (maxScroll <= epsilon) { 1471 prev.disabled = true; 1472 next.disabled = true; 1473 return; 1474 } 1475 1476 prev.disabled = left <= epsilon; 1477 next.disabled = left >= maxScroll - epsilon; 1478 } 1479 1480 function initViewport(viewport) { 1481 sanitizeCardSummaries(viewport); 1482 1483 var track = viewport.querySelector(".js-card-carousel-track"); 1484 var prev = viewport.querySelector(".js-card-carousel-prev"); 1485 var next = viewport.querySelector(".js-card-carousel-next"); 1486 1487 if (!track || !prev || !next) { 1488 return; 1489 } 1490 1491 function syncNav() { 1492 updateNav(track, prev, next); 1493 } 1494 1495 prev.addEventListener("click", function () { 1496 track.scrollBy({ left: -scrollStep(track), behavior: "smooth" }); 1497 }); 1498 1499 next.addEventListener("click", function () { 1500 track.scrollBy({ left: scrollStep(track), behavior: "smooth" }); 1501 }); 1502 1503 track.addEventListener("scroll", syncNav, { passive: true }); 1504 window.addEventListener("resize", syncNav); 1505 window.addEventListener("load", syncNav); 1506 1507 // scrollWidth often wrong on first frame (fonts, lazy images, % widths). 1508 // Resize on the inner row catches when horizontal content size is known. 1509 var row = track.querySelector(".card-carousel__container"); 1510 if (row && typeof ResizeObserver !== "undefined") { 1511 var ro = new ResizeObserver(function () { 1512 syncNav(); 1513 }); 1514 ro.observe(row); 1515 ro.observe(track); 1516 } 1517 1518 var imgs = track.querySelectorAll("img"); 1519 for (var j = 0; j < imgs.length; j++) { 1520 if (!imgs[j].complete) { 1521 imgs[j].addEventListener("load", syncNav); 1522 imgs[j].addEventListener("error", syncNav); 1523 } 1524 } 1525 1526 if (typeof requestAnimationFrame === "function") { 1527 requestAnimationFrame(function () { 1528 requestAnimationFrame(syncNav); 1529 }); 1530 } else { 1531 syncNav(); 1532 } 1533 } 1534 1535 function initAll() { 1536 var viewports = document.querySelectorAll(".js-card-carousel"); 1537 for (var i = 0; i < viewports.length; i++) { 1538 initViewport(viewports[i]); 1539 } 1540 } 1541 1542 if (document.readyState === "loading") { 1543 document.addEventListener("DOMContentLoaded", initAll); 1544 } else { 1545 initAll(); 1546 } 1547})(); 1548 1549var ADD_TO_CALENDAR_STORAGE_KEY = 'selectedSessions'; 1550 1551function _addToCal_escapeIcsText(text) { 1552 if (text == null || text === '') { 1553 return ''; 1554 } 1555 return String(text) 1556 .replace(/\\/g, '\\\\') 1557 .replace(/\n/g, '\\n') 1558 .replace(/;/g, '\\;') 1559 .replace(/,/g, '\\,'); 1560} 1561 1562function _addToCal_pad2(n) { 1563 return n < 10 ? '0' + n : String(n); 1564} 1565 1566function _addToCal_formatUtc(dt) { 1567 return ( 1568 dt.getUTCFullYear() + 1569 _addToCal_pad2(dt.getUTCMonth() + 1) + 1570 _addToCal_pad2(dt.getUTCDate()) + 1571 'T' + 1572 _addToCal_pad2(dt.getUTCHours()) + 1573 _addToCal_pad2(dt.getUTCMinutes()) + 1574 _addToCal_pad2(dt.getUTCSeconds()) + 1575 'Z' 1576 ); 1577} 1578 1579/** 1580 * Returns end Date from start and duration string (best-effort). Fallback: +1 hour. 1581 */ 1582function _addToCal_endFromDuration(startMs, durationStr) { 1583 var fallback = startMs + 60 * 60 * 1000; 1584 if (durationStr == null || String(durationStr).trim() === '') { 1585 return new Date(fallback); 1586 } 1587 var s = String(durationStr).trim(); 1588 var m; 1589 1590 if (/^P/i.test(s)) { 1591 try { 1592 var iso = s.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/i); 1593 if (iso) { 1594 var d = 0; 1595 var h = 0; 1596 var min = 0; 1597 var sec = 0; 1598 if (iso[1]) { 1599 d = parseInt(iso[1], 10) || 0; 1600 } 1601 if (iso[2]) { 1602 h = parseInt(iso[2], 10) || 0; 1603 } 1604 if (iso[3]) { 1605 min = parseInt(iso[3], 10) || 0; 1606 } 1607 if (iso[4]) { 1608 sec = parseInt(iso[4], 10) || 0; 1609 } 1610 var ms = (((d * 24 + h) * 60 + min) * 60 + sec) * 1000; 1611 if (ms > 0) { 1612 return new Date(startMs + ms); 1613 } 1614 } 1615 } catch (e) { 1616 /* fall through */ 1617 } 1618 } 1619 1620 if (/^\d+$/.test(s)) { 1621 var mins = parseInt(s, 10); 1622 if (!isNaN(mins) && mins > 0) { 1623 return new Date(startMs + mins * 60 * 1000); 1624 } 1625 } 1626 1627 m = s.match(/(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)\b/i); 1628 if (m) { 1629 var hours = parseFloat(m[1]); 1630 if (!isNaN(hours) && hours > 0) { 1631 return new Date(startMs + Math.round(hours * 60 * 60 * 1000)); 1632 } 1633 } 1634 1635 m = s.match(/(\d+(?:\.\d+)?)\s*(m|min|mins|minute|minutes)\b/i); 1636 if (m) { 1637 var minutes = parseFloat(m[1]); 1638 if (!isNaN(minutes) && minutes > 0) { 1639 return new Date(startMs + Math.round(minutes * 60 * 1000)); 1640 } 1641 } 1642 1643 m = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/); 1644 if (m) { 1645 var hh = parseInt(m[1], 10); 1646 var mm = parseInt(m[2], 10); 1647 var ss = m[3] ? parseInt(m[3], 10) : 0; 1648 if (!isNaN(hh) && !isNaN(mm)) { 1649 return new Date(startMs + ((hh * 60 + mm) * 60 + ss) * 1000); 1650 } 1651 } 1652 1653 return new Date(fallback); 1654} 1655 1656function _addToCal_foldLine(line) { 1657 var max = 75; 1658 if (line.length <= max) { 1659 return line; 1660 } 1661 var out = []; 1662 var rest = line; 1663 out.push(rest.substring(0, max)); 1664 rest = rest.substring(max); 1665 while (rest.length > 0) { 1666 out.push(' ' + rest.substring(0, max - 1)); 1667 rest = rest.substring(max - 1); 1668 } 1669 return out.join('\r\n'); 1670} 1671 1672function _addToCal_buildVevent(session, index, formUrl) { 1673 var startRaw = session && session.startDateTime; 1674 if (!startRaw) { 1675 return ''; 1676 } 1677 var start = new Date(startRaw); 1678 if (isNaN(start.getTime())) { 1679 return ''; 1680 } 1681 var startMs = start.getTime(); 1682 var end = _addToCal_endFromDuration(startMs, session.duration); 1683 var uidBase = 1684 (session.code || 'session') + 1685 '-' + 1686 _addToCal_formatUtc(start) + 1687 '-' + 1688 index +
1689 '@wsu-openday'; 1690 var uid = _addToCal_escapeIcsText(uidBase.replace(/[^a-zA-Z0-9@-]/g, '-')); 1691 1692 var rawTitle = (session.title || 'Session').trim(); 1693 var title = _addToCal_escapeIcsText('WSU Open Day: ' + rawTitle); 1694 var locParts = []; 1695 if (session.location) { 1696 locParts.push(String(session.location).trim()); 1697 } 1698 if (session.campus) { 1699 locParts.push(String(session.campus).trim()); 1700 } 1701 var location = _addToCal_escapeIcsText(locParts.filter(Boolean).join(' — ')); 1702 1703 var lines = [ 1704 'BEGIN:VEVENT', 1705 'UID:' + uid, 1706 'DTSTAMP:' + _addToCal_formatUtc(new Date()), 1707 'DTSTART:' + _addToCal_formatUtc(start), 1708 'DTEND:' + _addToCal_formatUtc(end), 1709 _addToCal_foldLine('SUMMARY:' + title) 1710 ]; 1711 if (location) { 1712 lines.push(_addToCal_foldLine('LOCATION:' + location)); 1713 } 1714 var descriptionParts = []; 1715 if (session.description) { 1716 descriptionParts.push(String(session.description).trim()); 1717 } 1718 if (formUrl && String(formUrl).trim() !== '') { 1719 descriptionParts.push(String(formUrl).trim()); 1720 } 1721 if (descriptionParts.length) { 1722 lines.push( 1723 _addToCal_foldLine('DESCRIPTION:' + _addToCal_escapeIcsText(descriptionParts.join('\n'))) 1724 ); 1725 } 1726 lines.push('END:VEVENT'); 1727 return lines.join('\r\n'); 1728} 1729 1730/** 1731 * Builds an iCalendar (.ics) from localStorage "selectedSessions" (same array shape as forms.js) and triggers download. 1732 * @param {string} [filename] optional download filename (default: openday-sessions.ics) 1733 * @returns {boolean} true if a file was generated and download started 1734 */ 1735function addToCalendar(filename, formUrl) { 1736 if (typeof localStorage === 'undefined') { 1737 return false; 1738 } 1739 var raw; 1740 try { 1741 raw = localStorage.getItem(ADD_TO_CALENDAR_STORAGE_KEY); 1742 } catch (e) { 1743 return false; 1744 } 1745 if (!raw) { 1746 return false; 1747 } 1748 var sessions; 1749 try { 1750 sessions = JSON.parse(raw); 1751 } catch (e) { 1752 return false; 1753 } 1754 if (!Array.isArray(sessions) || !sessions.length) { 1755 return false; 1756 } 1757 1758 var events = []; 1759 var i; 1760 for (i = 0; i < sessions.length; i++) { 1761 var block = _addToCal_buildVevent(sessions[i], i, formUrl); 1762 if (block) { 1763 events.push(block); 1764 } 1765 } 1766 if (!events.length) { 1767 return false; 1768 } 1769 1770 var icsBody = 1771 'BEGIN:VCALENDAR\r\n' + 1772 'VERSION:2.0\r\n' + 1773 'PRODID:-//WSU//Open Day Form//EN\r\n' + 1774 'CALSCALE:GREGORIAN\r\n' + 1775 'METHOD:PUBLISH\r\n' + 1776 events.join('\r\n') + 1777 '\r\nEND:VCALENDAR\r\n'; 1778 1779 var blob; 1780 try { 1781 blob = new Blob([icsBody], { type: 'text/calendar;charset=utf-8' }); 1782 } catch (e) { 1783 return false; 1784 } 1785 1786 var name = 1787 filename && String(filename).trim() !== '' 1788 ? String(filename).trim() 1789 : 'openday-sessions.ics'; 1790 if (name.indexOf('.ics') === -1) { 1791 name += '.ics'; 1792 } 1793 1794 var url = URL.createObjectURL(blob); 1795 var a = document.createElement('a'); 1796 a.href = url;
1797 a.download = name; 1798 a.rel = 'noopener'; 1799 document.body.appendChild(a); 1800 a.click(); 1801 document.body.removeChild(a); 1802 setTimeout(function () { 1803 URL.revokeObjectURL(url); 1804 }, 0); 1805 1806 return true; 1807} 1808 1809/** 1810 * Parses form payload strings into a plain object. 1811 * Examples: 1812 * - formName:Open Day Form,email:[email protected] 1813 * - formName:Open Day Form 1814 * Outer wrapping single/double quotes are stripped; each segment uses the first ':' as key/value separator (values may contain ':'). 1815 */ 1816function parseStringToObject(str) { 1817 const result = {}; 1818 1819 if (!str || typeof str !== 'string') { 1820 return result; 1821 } 1822 1823 let s = str.trim(); 1824 while ( 1825 (s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') || 1826 (s.charAt(0) === "'" && s.charAt(s.length - 1) === "'") 1827 ) { 1828 s = s.slice(1, -1).trim(); 1829 } 1830 1831 function addPair(partRaw) { 1832 let part = String(partRaw).trim(); 1833 if (!part) { 1834 return; 1835 } 1836 const colon = part.indexOf(':'); 1837 if (colon === -1) { 1838 return; 1839 } 1840 const key = part.slice(0, colon).trim(); 1841 const value = part.slice(colon + 1).trim(); 1842 if (key) { 1843 result[key] = value; 1844 } 1845 } 1846 1847 s.split(',').forEach(function (segment) { 1848 addPair(segment); 1849 }); 1850 1851 return result; 1852} 1853 1854function ensureDigitalDataEventData() { 1855 window.digitalData = window.digitalData || {}; 1856 window.digitalData.event = window.digitalData.event || {}; 1857 window.digitalData.event.data = window.digitalData.event.data || {}; 1858 return window.digitalData.event.data; 1859} 1860 1861function trackAnalytics(event, formData) { 1862 let formDataObject = parseStringToObject(formData); 1863 ensureDigitalDataEventData(); 1864 window.digitalData.event.data.form = formDataObject.formName; 1865 1866 switch (event) { 1867 case 'form_start_AA': 1868 _satellite.track(event, { 1869 form_name: window.digitalData.event.data.form 1870 }); 1871 break; 1872 case 'form_complete_AA': 1873 window.digitalData.event.data.email = formDataObject.email; 1874 _satellite.track('form_complete_AA', { 1875 form_name: window.digitalData.event.data.form, 1876 email: window.digitalData.event.data.email 1877 }); 1878 break; 1879 } 1880} 1881 1882/** 1883 * Country reference data shared by intl-tel.js. 1884 * 1885 * Entry: { n: display name, c: ISO 3166-1 alpha-2, d: dialling prefix }. 1886 * NANP territories carry their full prefix (e.g. "1876"); US/Canada stay "1". 1887 * WSU_COUNTRY_ALIASES maps an ISO code to alternative drop-down option names. 1888 */ 1889(function (global) { 1890 "use strict"; 1891 1892 var DATA = [ 1893 ["Afghanistan", "AF", "93"], ["Albania", "AL", "355"], ["Algeria", "DZ", "213"], 1894 ["Andorra", "AD", "376"], ["Angola", "AO", "244"], ["Antigua and Barbuda", "AG", "1268"], 1895 ["Argentina", "AR", "54"], ["Armenia", "AM", "374"], ["Aruba", "AW", "297"], 1896 ["Australia", "AU", "61"], ["Austria", "AT", "43"], ["Azerbaijan", "AZ", "994"], 1897 ["Bahamas", "BS", "1242"], ["Bahrain", "BH", "973"], ["Bangladesh", "BD", "880"], 1898 ["Barbados", "BB", "1246"], ["Belarus", "BY", "375"], ["Belgium", "BE", "32"], 1899 ["Belize", "BZ", "501"], ["Benin", "BJ", "229"], ["Bermuda", "BM", "1441"], 1900 ["Bhutan", "BT", "975"], ["Bolivia", "BO", "591"], ["Bosnia and Herzegovina", "BA", "387"], 1901 ["Botswana", "BW", "267"], ["Brazil", "BR", "55"], ["Brunei", "BN", "673"], 1902 ["Bulgaria", "BG", "359"], ["Burkina Faso", "BF", "226"], ["Burundi", "BI", "257"], 1903 ["Cambodia", "KH", "855"], ["Cameroon", "CM", "237"], ["Canada", "CA", "1"], 1904 ["Cape Verde", "CV", "238"], ["Cayman Islands", "KY", "1345"], 1905 ["Central African Republic", "CF", "236"], ["Chad", "TD", "235"], ["Chile", "CL", "56"], 1906 ["China", "CN", "86"], ["Colombia", "CO", "57"], ["Comoros", "KM", "269"], 1907 ["Congo (Brazzaville)", "CG", "242"], ["Congo (Kinshasa)", "CD", "243"], 1908 ["Costa Rica", "CR", "506"], ["Cote d'Ivoire", "CI", "225"], ["Croatia", "HR", "385"], 1909 ["Cuba", "CU", "53"], ["Cyprus", "CY", "357"], ["Czechia", "CZ", "420"], 1910 ["Denmark", "DK", "45"], ["Djibouti", "DJ", "253"], ["Dominica", "DM", "1767"], 1911 ["Dominican Republic", "DO", "1809"], ["Ecuador", "EC", "593"], ["Egypt", "EG", "20"], 1912 ["El Salvador", "SV", "503"], ["Equatorial Guinea", "GQ", "240"], ["
1912Eritrea", "ER", "291"], 1913 ["Estonia", "EE", "372"], ["Eswatini", "SZ", "268"], ["Ethiopia", "ET", "251"], 1914 ["Fiji", "FJ", "679"], ["Finland", "FI", "358"], ["France", "FR", "33"], 1915 ["French Guiana", "GF", "594"], ["French Polynesia", "PF", "689"], ["Gabon", "GA", "241"], 1916 ["Gambia", "GM", "220"], ["Georgia", "GE", "995"], ["Germany", "DE", "49"], 1917 ["Ghana", "GH", "233"], ["Gibraltar", "GI", "350"], ["Greece", "GR", "30"], 1918 ["Greenland", "GL", "299"], ["Grenada", "GD", "1473"], ["Guadeloupe", "GP", "590"], 1919 ["Guam", "GU", "1671"], ["Guatemala", "GT", "502"], ["Guinea", "GN", "224"], 1920 ["Guinea-Bissau", "GW", "245"], ["Guyana", "GY", "592"], ["Haiti", "HT", "509"], 1921 ["Honduras", "HN", "504"], ["Hong Kong", "HK", "852"], ["Hungary", "HU", "36"], 1922 ["Iceland", "IS", "354"], ["India", "IN", "91"], ["Indonesia", "ID", "62"], 1923 ["Iran", "IR", "98"], ["Iraq", "IQ", "964"], ["Ireland", "IE", "353"], 1924 ["Israel", "IL", "972"], ["Italy", "IT", "39"], ["Jamaica", "JM", "1876"], 1925 ["Japan", "JP", "81"], ["Jordan", "JO", "962"], ["Kazakhstan", "KZ", "7"], 1926 ["Kenya", "KE", "254"], ["Kiribati", "KI", "686"], ["Kosovo", "XK", "383"], 1927 ["Kuwait", "KW", "965"], ["Kyrgyzstan", "KG", "996"], ["Laos", "LA", "856"], 1928 ["Latvia", "LV", "371"], ["Lebanon", "LB", "961"], ["Lesotho", "LS", "266"], 1929 ["Liberia", "LR", "231"], ["Libya", "LY", "218"], ["Liechtenstein", "LI", "423"], 1930 ["Lithuania", "LT", "370"], ["Luxembourg", "LU", "352"], ["Macau", "MO", "853"], 1931 ["Madagascar", "MG", "261"], ["Malawi", "MW", "265"], ["Malaysia", "MY", "60"], 1932 ["Maldives", "MV", "960"], ["Mali", "ML", "223"], ["Malta", "MT", "356"], 1933 ["Marshall Islands", "MH", "692"], ["Martinique", "MQ", "596"], ["Mauritania", "MR", "222"], 1934 ["Mauritius", "MU", "230"], ["Mexico", "MX", "52"], ["Micronesia", "FM", "691"], 1935 ["Moldova", "MD", "373"], ["Monaco", "MC", "377"], ["Mongolia", "MN", "976"], 1936 ["Montenegro", "ME", "382"], ["Montserrat", "MS", "1664"], ["Morocco", "MA", "212"], 1937 ["Mozambique", "MZ", "258"], ["Myanmar", "MM", "95"], ["Namibia", "NA", "264"], 1938 ["Nauru", "NR", "674"], ["Nepal", "NP", "977"], ["Netherlands", "NL", "31"], 1939 ["New Caledonia", "NC", "687"], ["New Zealand", "NZ", "64"], ["Nicaragua", "NI", "505"],
1940 ["Niger", "NE", "227"], ["Nigeria", "NG", "234"], ["North Korea", "KP", "850"], 1941 ["North Macedonia", "MK", "389"], ["Norway", "NO", "47"], ["Oman", "OM", "968"], 1942 ["Pakistan", "PK", "92"], ["Palau", "PW", "680"], ["Palestine", "PS", "970"], 1943 ["Panama", "PA", "507"], ["Papua New Guinea", "PG", "675"], ["Paraguay", "PY", "595"], 1944 ["Peru", "PE", "51"], ["Philippines", "PH", "63"], ["Poland", "PL", "48"], 1945 ["Portugal", "PT", "351"], ["Puerto Rico", "PR", "1787"], ["Qatar", "QA", "974"], 1946 ["Reunion", "RE", "262"], ["Romania", "RO", "40"], ["Russia", "RU", "7"], 1947 ["Rwanda", "RW", "250"], ["Saint Kitts and Nevis", "KN", "1869"], ["Saint Lucia", "LC", "1758"], 1948 ["Saint Vincent and the Grenadines", "VC", "1784"], ["Samoa", "WS", "685"], 1949 ["San Marino", "SM", "378"], ["Sao Tome and Principe", "ST", "239"], 1950 ["Saudi Arabia", "SA", "966"], ["Senegal", "SN", "221"], ["Serbia", "RS", "381"], 1951 ["Seychelles", "SC", "248"], ["Sierra Leone", "SL", "232"], ["Singapore", "SG", "65"], 1952 ["Slovakia", "SK", "421"], ["Slovenia", "SI", "386"], ["Solomon Islands", "SB", "677"], 1953 ["Somalia", "SO", "252"], ["South Africa", "ZA", "27"], ["South Korea", "KR", "82"], 1954 ["South Sudan", "SS", "211"], ["Spain", "ES", "34"], ["Sri Lanka", "LK", "94"], 1955 ["Sudan", "SD", "249"], ["Suriname", "SR", "597"], ["Sweden", "SE", "46"], 1956 ["Switzerland", "CH", "41"], ["Syria", "SY", "963"], ["Taiwan", "TW", "886"], 1957 ["Tajikistan", "TJ", "992"], ["Tanzania", "TZ", "255"], ["Thailand", "TH", "66"], 1958 ["Timor-Leste", "TL", "670"], ["Togo", "TG", "228"], ["Tonga", "TO", "676"], 1959 ["Trinidad and Tobago", "TT", "1868"], ["Tunisia", "TN", "216"], ["Turkey", "TR", "90"], 1960 ["Turkmenistan", "TM", "993"], ["Turks and Caicos Islands", "TC", "1649"], 1961 ["Tuvalu", "TV", "688"], ["Uganda", "UG", "256"], ["Ukraine", "UA", "380"], 1962 ["United Arab Emirates", "AE", "971"], ["United Kingdom", "GB", "44"], 1963 ["United States", "US", "1"], ["Uruguay", "UY", "598"], ["Uzbekistan", "UZ", "998"], 1964 ["Vanuatu", "VU", "678"], ["Vatican City", "VA", "379"], ["Venezuela", "VE", "58"], 1965 ["Vietnam", "VN", "84"], ["Yemen", "YE", "967"], ["Zambia", "ZM", "260"], 1966 ["Zimbabwe", "ZW", "263"] 1967 ]; 1968 1969 var ALIASES = { 1970 US: ["usa", "u s a", "united states of america", "america"], 1971 GB: ["uk", "u k", "great britain", "britain", "england", "scotland", "wales", "northern ireland"], 1972 KR: ["korea", "republic of korea", "korea republic of", "korea south", "south korea"], 1973 KP: ["korea democratic peoples republic of", "korea north", "dprk"], 1974 CD: ["democratic republic of the congo", "congo democratic republic of the", "drc", "congo drc", "zaire"], 1975 CG: ["republic of the congo", "congo republic of the", "congo"], 1976 CI: ["ivory coast", "cote divoire"], 1977 CZ: ["czech republic"], 1978 SZ: ["swaziland"], 1979 MK: ["macedonia", "former yugoslav republic of macedonia"], 1980 MM: ["burma"], 1981 TL: ["east timor"], 1982 CV: ["cabo verde"], 1983 VA: ["holy see", "holy see vatican city state"], 1984 RU: ["russian federation"], 1985 SY: ["syrian arab republic"], 1986 IR: ["iran islamic republic of"], 1987 LA: ["lao peoples democratic republic"], 1988 MD: ["moldova republic of"], 1989 TZ: ["tanzania united republic of"], 1990 VN: ["viet nam"], 1991 BN: ["brunei darussalam"], 1992 BO: ["bolivia plurinational state of"], 1993 VE: ["venezuela bolivarian republic of"], 1994 PS: ["palestinian territory", "palestinian territory occupied", "state of palestine"], 1995 HK: ["hong kong sar", "hong kong sar china", "hong kong special administrative region"], 1996 MO: ["macao", "macao sar", "macau sar"] 1997 }; 1998 1999 global.WSU_COUNTRY_DATA = DATA.map(function (row) { 2000 return { n: row[0], c: row[1], d: row[2] }; 2001 }); 2002 2003 global.WSU_COUNTRY_ALIASES = ALIASES; 2004 2005 /* Preferred ISO codes, most relevant first. */ 2006 global.WSU_COUNTRY_PREFERRED = ["AU", "NZ", "CN", "IN", "GB", "US"]; 2007 2008 /* ISO alpha-2 -> regional-indicator flag emoji. */ 2009 global.wsuCountryFlag = function (iso2) { 2010 if (!iso2 || iso2.length !== 2) { 2011 return ""; 2012 } 2013 var base = 127397; 2014 return String.fromCodePoint(base + iso2.toUpperCase().charCo
2014deAt(0)) + 2015 String.fromCodePoint(base + iso2.toUpperCase().charCodeAt(1)); 2016 }; 2017})(typeof window !== "undefined" ? window : this); 2018 2019/** 2020 * Country calling-code picker for an Adaptive Form phone field. 2021 * 2022 * Progressive enhancement: attaches a flag + calling-code picker to the phone 2023 * field and, for a non-default country, pre-selects the "country of residence" 2024 * drop-down. The visible input keeps the national number only; the picked code 2025 * is exposed as data-country-code on the field input and wrapper. 2026 * 2027 * Activation: field Element name "phoneNumber" (extend via 2028 * window.WSU_INTL_TEL_FIELDS), or class "intl-tel" / attribute data-intl-tel on 2029 * the field or an ancestor. Force off with class "no-intl-tel" or 2030 * data-intl-tel="false". Optional field attributes: data-intl-tel-residence, 2031 * data-intl-tel-default (ISO-2, defaults to AU). 2032 * 2033 * API: window.wsuIntlTel.getCountryCode(name) / getFullNumber(name). 2034 */ 2035(function (global) { 2036 "use strict"; 2037 2038 var doc = global.document; 2039 if (!doc) { 2040 return; 2041 } 2042 2043 var FIELD_SELECTOR = ".cmp-adaptiveform-telephoneinput, .cmp-adaptiveform-textinput"; 2044 var WIDGET_SELECTOR = 2045 ".cmp-adaptiveform-telephoneinput__widget, .cmp-adaptiveform-textinput__widget"; 2046 var DEFAULT_ISO = "AU"; 2047 var AU_ISO = "AU"; 2048 var FONT_URL = 2049 "/etc.clientlibs/wsu-international/clientlibs/clientlib-forms/resources/fonts/TwemojiCountryFlags.woff2"; 2050 2051 var COUNTRIES = (global.WSU_COUNTRY_DATA || []).slice().sort(function (a, b) { 2052 return a.n.localeCompare(b.n); 2053 }); 2054 var ALIASES = global.WSU_COUNTRY_ALIASES || {}; 2055 var flagFor = global.wsuCountryFlag || function () { return ""; }; 2056 2057 /* Field Element names picked up by name (lower-cased for comparison). */ 2058 var NAME_MATCHES = (global.WSU_INTL_TEL_FIELDS || ["phoneNumber"]) 2059 .map(function (s) { return String(s).toLowerCase(); }); 2060 2061 var internalResidenceChange = false; 2062 var widgetSeq = 0; 2063 2064 /* -------------------------------------------------------------- lookups -- */ 2065 2066 var byIso = {}; 2067 COUNTRIES.forEach(function (c) { 2068 byIso[c.c] = c; 2069 }); 2070 2071 /* Longest dial code first so "+1876" beats "+1", "+61" is unambiguous, etc. */ 2072 var DIAL_MATCH_ORDER = COUNTRIES.slice().sort(function (a, b) { 2073 if (b.d.length !== a.d.length) { 2074 return b.d.length - a.d.length; 2075 } 2076 return a.n.localeCompare(b.n); 2077 }); 2078 2079 function countryByDialDigits(digits) { 2080 if (!digits) { 2081 return null; 2082 } 2083 for (var i = 0; i < DIAL_MATCH_ORDER.length; i++) { 2084 if (digits.indexOf(DIAL_MATCH_ORDER[i].d) === 0) { 2085 return DIAL_MATCH_ORDER[i]; 2086 } 2087 } 2088 return null; 2089 } 2090 2091 function defaultCountryFor(field) { 2092 var iso = (field.getAttribute("data-intl-tel-default") || DEFAULT_ISO).toUpperCase(); 2093 return byIso[iso] || byIso[DEFAULT_ISO] || COUNTRIES[0]; 2094 } 2095 2096 /* ---------------------------------------------------------- text helpers -- */ 2097 2098 function normalize(str) { 2099 var s = String(str == null ? "" : str).toLowerCase(); 2100 if (typeof s.normalize === "function") { 2101 s = s.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 2102 } 2103 return s.replace(/[^a-z0-9]+/g, " ").trim(); 2104 } 2105 2106 function digitsOnly(str) { 2107 return String(str == null ? "" : str).replace(/\D/g, ""); 2108 } 2109 2110 function nationalPart(raw, prevDialDigits) { 2111 var v = String(raw == null ? "" : raw).trim(); 2112 if (v.charAt(0) !== "+") { 2113 return digitsOnly(v); 2114 } 2115 var digits = digitsOnly(v); 2116 var prev = digitsOnly(prevDialDigits || ""); 2117 if (prev && digits.indexOf(prev) === 0) { 2118 return digits.slice(prev.length); 2119 } 2120 var match = countryByDialDigits(digits); 2121 return match ? digits.slice(match.d.length) : digits; 2122 } 2123 2124 /* ------------------------------------------------ country of residence -- */ 2125 2126 function cssEscape(value) { 2127 if (global.CSS && typeof global.CSS.escape === "function") { 2128 return global.CSS.escape(value); 2129 } 2130 return String(value).replace(/["\\]/g, "\\$&"); 2131 } 2132 2133 function residenceSelectFor(field) { 2134 var explicit = field.getAttribute("data-intl-tel-residence"); 2135 if (explicit) { 2136 return doc.querySelector('select[name="' + cssEscape(explicit) + '"]'); 2137 } 2138 var selects = doc.querySelectorAll("select"); 2139 for (var i = 0; i < selects.length; i++) { 2140 var s = selects[i]; 2141 if (/residen/i.test((s.getAttribute("name") || "") + " " + (s.id || ""))) { 2142 return s; 2143 } 2144 var wrap = s.closest("[class*='adaptiveform']"); 2145 var label = wrap && wrap.querySelector("label, [class*='__label']"); 2146 if (label && /country of residence/i.test(label.textContent || "")) { 2147 return s; 2148 } 2149 } 2150 return null; 2151 } 2152 2153 function matchResidenceOption(select, country) { 2154 var wanted = [normalize(country.n)];
2155 (ALIASES[country.c] || []).forEach(function (a) { 2156 wanted.push(normalize(a)); 2157 }); 2158 var iso = country.c.toLowerCase(); 2159 for (var i = 0; i < select.options.length; i++) { 2160 var opt = select.options[i]; 2161 if (opt.value === "") { 2162 continue; 2163 } 2164 var text = normalize(opt.textContent).replace(/^the /, ""); 2165 if (wanted.indexOf(text) !== -1 || 2166 wanted.indexOf(normalize(opt.value)) !== -1 || 2167 String(opt.value).toLowerCase() === iso) { 2168 return opt; 2169 } 2170 } 2171 return null; 2172 } 2173 2174 function bindResidenceUserTouch(select) { 2175 if (select.dataset.intlTelResidenceBound === "true") { 2176 return; 2177 } 2178 select.dataset.intlTelResidenceBound = "true"; 2179 select.addEventListener("change", function () { 2180 if (internalResidenceChange) { 2181 return; 2182 } 2183 select.dataset.intlTelUserTouched = "true"; 2184 select.dataset.intlTelAutofilled = "false"; 2185 }); 2186 } 2187 2188 function setResidenceValue(select, value) { 2189 if (select.value === value) { 2190 return true; 2191 } 2192 internalResidenceChange = true; 2193 select.value = value; 2194 var applied = select.value === value || value === ""; 2195 if (applied) { 2196 select.dispatchEvent(new Event("input", { bubbles: true })); 2197 select.dispatchEvent(new Event("change", { bubbles: true })); 2198 } 2199 internalResidenceChange = false; 2200 return applied; 2201 } 2202 2203 function syncResidence(field, country) { 2204 var select = residenceSelectFor(field); 2205 if (!select || select.multiple) { 2206 return; 2207 } 2208 bindResidenceUserTouch(select); 2209 2210 var autofilled = select.dataset.intlTelAutofilled === "true"; 2211 var userTouched = select.dataset.intlTelUserTouched === "true"; 2212 var hasValue = select.value !== "" && select.value != null; 2213 2214 // Never overwrite a value the visitor picked or that was set elsewhere. 2215 if (!autofilled && (userTouched || hasValue)) { 2216 return; 2217 } 2218 2219 if (country.c === AU_ISO || country.d === "61") { 2220 if (autofilled) { 2221 setResidenceValue(select, ""); 2222 select.dataset.intlTelAutofilled = "false"; 2223 } 2224 return; 2225 } 2226 2227 var opt = matchResidenceOption(select, country); 2228 if (opt && setResidenceValue(select, opt.value)) { 2229 select.dataset.intlTelAutofilled = "true"; 2230 } 2231 } 2232 2233 /* ----------------------------------------------------------------- CSS -- */ 2234 2235 function injectStyles() { 2236 if (doc.getElementById("wsu-intl-tel-styles")) { 2237 return; 2238 } 2239 var css = 2240 '@font-face{font-family:"Twemoji Country Flags";' + 2241 'src:url("' + FONT_URL + '") format("woff2");font-display:swap;' + 2242 'unicode-range:U+1F1E6-1F1FF,U+1F3F4,U+E0060-E007F}' + 2243 '.wsu-intl-tel{position:relative;display:block}' + 2244 '.wsu-intl-tel__input.wsu-intl-tel__input{padding-left:4rem}' + 2245 '.wsu-intl-tel__toggle{position:absolute;left:1px;top:1px;bottom:1px;display:flex;' + 2246 'align-items:center;gap:.25rem;margin:0;margin-top:12px;padding:0 .5rem;' + 2247 'background:transparent;border:0;border-right:1px solid rgba(0,0,0,.2);border-radius:0;' + 2248 'font:inherit;line-height:1;color:inherit;cursor:pointer}' + 2249 '.wsu-intl-tel__toggle:hover{background:rgba(0,0,0,.04)}' + 2250 '.wsu-intl-tel__toggle:focus-visible{outline:2px solid currentColor;outline-offset:-2px}' + 2251 '.wsu-intl-tel__flag{display:inline-block;min-width:1.4em;font-size:1.15em;line-height:1;' + 2252 'text-align:center;letter-spacing:0;font-weight:400;font-style:normal;' + 2253 'font-family:"Twemoji Country Flags","Noto Color Emoji","Apple Color Emoji",' + 2254 '"Segoe UI Emoji","Segoe UI Symbol",sans-serif}' + 2255 '.wsu-intl-tel__caret{width:0;height:0;border-left:4px solid transparent;' + 2256 'border-right:4px solid transparent;border-top:5px solid currentColor;opacity:.55}' + 2257 '.wsu-intl-tel.is-open .wsu-intl-tel__caret{transform:rotate(180deg)}' + 2258 '.wsu-intl-tel__panel{display:none;position:absolute;z-index:60;top:calc(100% + 2px);' + 2259 'left:0;width:340px;max-width:92vw;background:#fff;color:#000;' + 2260 'border:1px solid rgba(0,0,0,.25);border-radius:4px;box-shadow:0 6px 24px rgba(0,0,0,.18);' + 2261 'overflow:hidden}' + 2262 '.wsu-intl-tel.is-open .wsu-intl-tel__panel{display:block}' + 2263 '.wsu-intl-tel__list{list-style:none;margin:0;padding:.25rem 0;max-height:264px;' + 2264 'overflow-y:auto}' +
2265 '.wsu-intl-tel__option{display:flex;align-items:center;gap:.6rem;padding:.5rem .75rem;' + 2266 'font:inherit;cursor:pointer;outline:none}' + 2267 '.wsu-intl-tel__option.is-active,.wsu-intl-tel__option:hover{background:#ececec}' + 2268 '.wsu-intl-tel__option[aria-selected="true"]{font-weight:700}' + 2269 '.wsu-intl-tel__option-name{flex:1 1 auto}' + 2270 '.wsu-intl-tel__option-dial{flex:0 0 auto;color:rgba(0,0,0,.6)}' + 2271 '[dir="rtl"] .wsu-intl-tel__input.wsu-intl-tel__input{padding-left:0;padding-right:4rem}' + 2272 '[dir="rtl"] .wsu-intl-tel__toggle{left:auto;right:1px;border-right:0;' + 2273 'border-left:1px solid rgba(0,0,0,.2)}' + 2274 '[dir="rtl"] .wsu-intl-tel__panel{left:auto;right:0}'; 2275 var style = doc.createElement("style"); 2276 style.id = "wsu-intl-tel-styles"; 2277 style.textContent = css; 2278 (doc.head || doc.documentElement).appendChild(style); 2279 } 2280 2281 /* --------------------------------------------------------------- widget -- */ 2282 2283 /* Precedence: force off ("no-intl-tel" / data-intl-tel="false"), then force on 2284 ("intl-tel" class / data-intl-tel), then match on field Element name. */ 2285 function isActivated(field, input) { 2286 if ((input && input.closest && input.closest(".no-intl-tel")) || 2287 field.closest(".no-intl-tel") || 2288 field.getAttribute("data-intl-tel") === "false") { 2289 return false; 2290 } 2291 if ((input && input.closest && input.closest(".intl-tel")) || 2292 field.closest(".intl-tel") || 2293 (field.hasAttribute("data-intl-tel") && 2294 field.getAttribute("data-intl-tel") !== "false")) { 2295 return true; 2296 } 2297 var name = String( 2298 field.getAttribute("data-field-name") || (input && input.name) || "" 2299 ).toLowerCase(); 2300 return name !== "" && NAME_MATCHES.indexOf(name) !== -1; 2301 } 2302 2303 function buildOption(country, isSelected, id) { 2304 var li = doc.createElement("li"); 2305 li.className = "wsu-intl-tel__option"; 2306 li.id = id; 2307 li.setAttribute("role", "option"); 2308 li.setAttribute("tabindex", "-1"); 2309 li.setAttribute("aria-selected", isSelected ? "true" : "false"); 2310 li.dataset.iso = country.c; 2311 li.dataset.search = normalize(country.n + " " + country.c + " " + country.d); 2312 2313 var flag = doc.createElement("span"); 2314 flag.className = "wsu-intl-tel__flag"; 2315 flag.textContent = flagFor(country.c); 2316 var name = doc.createElement("span"); 2317 name.className = "wsu-intl-tel__option-name"; 2318 name.textContent = country.n; 2319 var dial = doc.createElement("span"); 2320 dial.className = "wsu-intl-tel__option-dial"; 2321 dial.textContent = "+" + country.d; 2322 2323 li.appendChild(flag); 2324 li.appendChild(name); 2325 li.appendChild(dial); 2326 return li; 2327 } 2328 2329 function enhanceField(field) { 2330 if (field.dataset.intlTelReady === "true" && field.querySelector(".wsu-intl-tel")) { 2331 return; 2332 } 2333 var input = field.querySelector(WIDGET_SELECTOR) || 2334 field.querySelector('input[type="tel"]') || field.querySelector("input"); 2335 if (!input || !isActivated(field, input)) { 2336 return; 2337 } 2338 field.dataset.intlTelReady = "true"; 2339 injectStyles(); 2340 2341 var uid = "wsu-intl-tel-" + (++widgetSeq); 2342 var state = { country: defaultCountryFor(field) }; 2343 2344 var wrap = doc.createElement("div"); 2345 wrap.className = "wsu-intl-tel"; 2346 input.parentNode.insertBefore(wrap, input); 2347 wrap.appendChild(input); 2348 input.classList.add("wsu-intl-tel__input"); 2349 2350 var toggle = doc.createElement("button"); 2351 toggle.type = "button"; 2352 toggle.className = "wsu-intl-tel__toggle"; 2353 toggle.setAttribute("aria-haspopup", "listbox"); 2354 toggle.setAttribute("aria-expanded", "false"); 2355 toggle.innerHTML = 2356 '<span class="wsu-intl-tel__flag" data-hook="flag"></span>' + 2357 '<span class="wsu-intl-tel__caret" aria-hidden="true"></span>'; 2358 wrap.insertBefore(toggle, input); 2359 2360 var panel = doc.createElement("div"); 2361 panel.className = "wsu-intl-tel__panel"; 2362 panel.innerHTML = 2363 '<ul class="wsu-intl-tel__list" id="' + uid + '-list" role="listbox" ' + 2364 'tabindex="-1" aria-label="Select country calling code"></ul>'; 2365 wrap.appendChild(panel); 2366 2367 var list = panel.querySelector(".wsu-intl-tel__list"); 2368 var flagHook = toggle.querySelector('[data-hook="flag"]'); 2369 var optionEls = []; 2370 2371 COUNTRIES.forEach(function (country) { 2372 var el = buildOption(country, country.c === state.country.c, 2373 uid + "-opt-" + optionEls.length); 2374 list.appendChild(el); 2375 optionEls.push(el); 2376 }); 2377 2378 function renderToggle() { 2379 flagHook.textContent = flagFor(state.country.c); 2380 var caption = state.country.n + " (+" + state.country.d + ")"; 2381 toggle.title = caption; 2382 toggle.setAttribute("aria-label", "Country calling code: " + caption);
2383 optionEls.forEach(function (el) { 2384 el.setAttribute("aria-selected", el.dataset.iso === state.country.c ? "true" : "false"); 2385 }); 2386 syncDataAttrs(); 2387 } 2388 2389 /* Expose the selected calling code as data-country-code on input + wrapper. */ 2390 function syncDataAttrs() { 2391 var code = "+" + state.country.d; 2392 input.setAttribute("data-country-code", code); 2393 field.setAttribute("data-country-code", code); 2394 } 2395 2396 /* The input only ever holds the national number (no "+code"). If a visitor 2397 types or pastes a "+.." value, adopt the country and strip it back. */ 2398 function stripPrefixToNational() { 2399 var raw = (input.value || "").trim(); 2400 if (raw.charAt(0) === "+") { 2401 var c = countryByDialDigits(digitsOnly(raw)); 2402 var national = nationalPart(raw, c ? c.d : state.country.d); 2403 if (c && c.c !== state.country.c) { 2404 state.country = c; 2405 renderToggle(); 2406 syncResidence(field, c); 2407 } 2408 if (input.value !== national) { 2409 input.value = national; 2410 input.dispatchEvent(new Event("input", { bubbles: true })); 2411 input.dispatchEvent(new Event("change", { bubbles: true })); 2412 } 2413 } 2414 syncDataAttrs(); 2415 } 2416 2417 function setCountry(country) { 2418 state.country = country; 2419 renderToggle(); 2420 syncResidence(field, country); 2421 } 2422 2423 renderToggle(); 2424 (function initFromValue() { 2425 var raw = (input.value || "").trim(); 2426 if (raw.charAt(0) === "+") { 2427 var c = countryByDialDigits(digitsOnly(raw)); 2428 if (c) { 2429 state.country = c; 2430 } 2431 input.value = nationalPart(raw, c ? c.d : state.country.d); 2432 } 2433 renderToggle(); 2434 syncResidence(field, state.country); 2435 })(); 2436 2437 input.addEventListener("focus", stripPrefixToNational); 2438 input.addEventListener("input", stripPrefixToNational); 2439 input.addEventListener("blur", stripPrefixToNational); 2440 2441 field._wsuIntlTelSync = syncDataAttrs; 2442 2443 /* ---- panel: no search box, matches intl-tel-input ---- */ 2444 2445 var isOpen = false; 2446 var activeOption = null; 2447 var typeBuffer = ""; 2448 var typeTimer = null; 2449 2450 function setActive(el, focusIt) { 2451 if (activeOption) { 2452 activeOption.classList.remove("is-active"); 2453 } 2454 activeOption = el || null; 2455 if (!activeOption) { 2456 list.removeAttribute("aria-activedescendant"); 2457 return; 2458 } 2459 activeOption.classList.add("is-active"); 2460 activeOption.scrollIntoView({ block: "nearest" }); 2461 list.setAttribute("aria-activedescendant", activeOption.id); 2462 if (focusIt !== false) { 2463 activeOption.focus(); 2464 } 2465 } 2466 2467 function currentOption() { 2468 for (var i = 0; i < optionEls.length; i++) { 2469 if (optionEls[i].dataset.iso === state.country.c) { 2470 return optionEls[i]; 2471 } 2472 } 2473 return optionEls[0]; 2474 } 2475 2476 function openPanel() { 2477 if (isOpen) { 2478 return; 2479 } 2480 isOpen = true; 2481 wrap.classList.add("is-open"); 2482 toggle.setAttribute("aria-expanded", "true"); 2483 setActive(currentOption()); 2484 doc.addEventListener("click", onDocClick, true); 2485 } 2486 2487 function closePanel(focusToggle) { 2488 if (!isOpen) { 2489 return; 2490 } 2491 isOpen = false; 2492 wrap.classList.remove("is-open"); 2493 toggle.setAttribute("aria-expanded", "false"); 2494 if (activeOption) { 2495 activeOption.classList.remove("is-active"); 2496 activeOption = null; 2497 } 2498 list.removeAttribute("aria-activedescendant"); 2499 doc.removeEventListener("click", onDocClick, true); 2500 if (focusToggle) { 2501 toggle.focus(); 2502 } 2503 } 2504 2505 function onDocClick(e) { 2506 if (!wrap.contains(e.target)) { 2507 closePanel(); 2508 } 2509 } 2510 2511 function choose(el) { 2512 if (!el || !el.dataset || !byIso[el.dataset.iso]) { 2513 return; 2514 } 2515 setCountry(byIso[el.dataset.iso]); 2516 closePanel(); 2517 input.focus(); 2518 try { 2519 input.setSelectionRange(input.value.length, input.value.length); 2520 } catch (e) { /* noop */ } 2521 } 2522 2523 function move(delta) { 2524 if (!optionEls.length) { 2525 return; 2526 } 2527 var idx = optionEls.indexOf(activeOption); 2528 idx = idx === -1 ? 0 : idx + delta;
2529 setActive(optionEls[Math.max(0, Math.min(optionEls.length - 1, idx))]); 2530 } 2531 2532 function typeahead(ch) { 2533 if (typeTimer) { 2534 global.clearTimeout(typeTimer); 2535 } 2536 typeBuffer += ch.toLowerCase(); 2537 typeTimer = global.setTimeout(function () { typeBuffer = ""; }, 800); 2538 for (var i = 0; i < optionEls.length; i++) { 2539 if (optionEls[i].dataset.search.indexOf(typeBuffer) === 0) { 2540 setActive(optionEls[i]); 2541 return; 2542 } 2543 } 2544 } 2545 2546 toggle.addEventListener("click", function () { 2547 if (isOpen) { 2548 closePanel(); 2549 } else { 2550 openPanel(); 2551 } 2552 }); 2553 2554 toggle.addEventListener("keydown", function (e) { 2555 if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { 2556 e.preventDefault(); 2557 openPanel(); 2558 } 2559 }); 2560 2561 list.addEventListener("click", function (e) { 2562 var el = e.target.closest(".wsu-intl-tel__option"); 2563 if (el) { 2564 choose(el); 2565 } 2566 }); 2567 2568 list.addEventListener("mousemove", function (e) { 2569 var el = e.target.closest(".wsu-intl-tel__option"); 2570 if (el && el !== activeOption) { 2571 setActive(el, false); 2572 } 2573 }); 2574 2575 list.addEventListener("keydown", function (e) { 2576 switch (e.key) { 2577 case "ArrowDown": e.preventDefault(); move(1); break; 2578 case "ArrowUp": e.preventDefault(); move(-1); break; 2579 case "Home": e.preventDefault(); setActive(optionEls[0]); break; 2580 case "End": e.preventDefault(); setActive(optionEls[optionEls.length - 1]); break; 2581 case "Enter": 2582 case " ": e.preventDefault(); choose(activeOption); break; 2583 case "Escape": e.preventDefault(); closePanel(true); break; 2584 case "Tab": closePanel(); break; 2585 default: 2586 if (e.key && e.key.length === 1 && /\S/.test(e.key)) { 2587 typeahead(e.key); 2588 } 2589 } 2590 }); 2591 2592 field._wsuIntlTel = { 2593 resync: function () { 2594 syncResidence(field, state.country); 2595 } 2596 }; 2597 } 2598 2599 /* --------------------------------------------------------------- driver -- */ 2600 2601 function enhanceAll() { 2602 Array.prototype.forEach.call(doc.querySelectorAll(FIELD_SELECTOR), enhanceField); 2603 } 2604 2605 function resyncAll() { 2606 Array.prototype.forEach.call(doc.querySelectorAll(FIELD_SELECTOR), function (field) { 2607 if (field._wsuIntlTel) { 2608 field._wsuIntlTel.resync(); 2609 } 2610 }); 2611 } 2612 2613 /* Refresh the data-intl-tel-* attributes on every enhanced field. Runs on any 2614 submit so the receiving side reads a current value. */ 2615 function syncAll() { 2616 Array.prototype.forEach.call(doc.querySelectorAll(FIELD_SELECTOR), function (field) { 2617 if (typeof field._wsuIntlTelSync === "function") { 2618 field._wsuIntlTelSync(); 2619 } 2620 }); 2621 } 2622 2623 var SUBMIT_SELECTOR = 2624 '.cmp-adaptiveform-button__widget,button[type="submit"],input[type="submit"]'; 2625 2626 function onDocClickCapture(e) { 2627 var t = e.target; 2628 if (t && t.closest && t.closest(SUBMIT_SELECTOR)) { 2629 syncAll(); 2630 } 2631 } 2632 2633 var scheduled = false; 2634 function scheduleScan() { 2635 if (scheduled) { 2636 return; 2637 } 2638 scheduled = true; 2639 global.setTimeout(function () { 2640 scheduled = false; 2641 enhanceAll(); 2642 resyncAll(); 2643 }, 150); 2644 } 2645 2646 function start() { 2647 enhanceAll(); 2648 doc.addEventListener("click", onDocClickCapture, true); 2649 doc.addEventListener("submit", syncAll, true); 2650 if (typeof global.MutationObserver === "function") { 2651 new global.MutationObserver(scheduleScan).observe(doc.body, { 2652 childList: true, 2653 subtree: true 2654 }); 2655 } 2656 } 2657 2658 if (doc.readyState === "loading") { 2659 doc.addEventListener("DOMContentLoaded", start); 2660 } else { 2661 start(); 2662 } 2663 2664 function fieldInputByName(name) { 2665 return doc.querySelector('[data-country-code][name="' + name + '"]') || 2666 doc.querySelector('[data-country-code][data-field-name="' + name + '"] input') || 2667 doc.querySelector('[name="' + name + '"]'); 2668 } 2669 2670 function fullNumberFor(name) { 2671 var el = fieldInputByName(name); 2672 if (!el) { 2673 return ""; 2674 } 2675 var codeDigits = (el.getAttribute("data-country-code") || "").replace(/\D/g, ""); 2676 var national = String(el.value || "").replace(/\D/g, "").replace(/^0/, ""); 2677 return national ? codeDigits + national : ""; 2678 } 2679 2680 global.wsuIntlTel = { 2681 refresh: enhanceAll, 2682 resyncResidence: resyncAll, 2683 sync: syncAll, 2684 getCountryCode: function (name) { 2685 var el = fieldInputByName(name); 2686 return el ? (el.getAttribute("data-country-code") || "") : ""; 2687 }, 2688 /* Dial-code digits + national digits (no "+", no leading trunk "0"), 2689 e.g. "61412345678" */ 2690 getFullNumber: fullNumberFor 2691 }; 2692})(typeof window !== "undefined" ? window : this);
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.