1/************************************************ 2 #Branded Pages 3 ************************************************/ 4var smsVerificationId = null; 5var firstPageLoad = true; 6 7jQuery(function (e) { 8 e.datepicker.regional.nl = 9 { 10 closeText: "Sluiten", 11 prevText: "Vorige", 12 nextText: "Volgende", 13 currentText: "Vandaag", 14 monthNames: ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"], 15 monthNamesShort: ["jan", "feb", "maa", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"], 16 dayNames: ["zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"], 17 dayNamesShort: ["zon", "maa", "din", "woe", "don", "vri", "zat"], 18 dayNamesMin: ["zo", "ma", "di", "wo", "do", "vr", "za"], 19 weekHeader: "Wk", 20 dateFormat: "dd/mm/yy", 21 firstDay: 1, 22 isRTL: !1, 23 showMonthAfterYear: !1, yearSuffix: "" 24 }, 25 e.datepicker.regional.en = 26 { 27 closeText: "Done", 28 prevText: "Previous", 29 nextText: "Next", 30 currentText: "Today", 31 monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
31November", "December"], 32 monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], 33 dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], 34 dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], 35 dayNamesMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], 36 weekHeader: "Wk", 37 dateFormat: "dd/mm/yy", 38 firstDay: 1, 39 isRTL: !1, 40 showMonthAfterYear: !1, 41 yearSuffix: "" 42 }, 43 e.datepicker.regional.fr = 44 { 45 closeText: "Fermer", 46 prevText: "Précédent", 47 nextText: "Suivant", 48 currentText: "Aujourd'hui", 49 monthNames: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"], 50 monthNamesShort: ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."], 51 dayNames: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"], 52 dayNamesShort: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."], 53 dayNamesMin: ["D", "L", "M", "M", "J", "V", "S"], 54 weekHeader: "Sem.", 55 dateFormat: "dd/mm/yy", 56 firstDay: 1, 57 isRTL: !1, 58 showMonthAfterYear: !1, 59 yearSuffix: "" 60 }, 61 e.datepicker.regional.de = 62 { 63 closeText: "SchlieÃen", 64 prevText: "<Zurück", 65 nextText: "Vor>", 66 currentText: "Heute", 67 monthNames: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], 68 monthNamesShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"], 69 dayNames: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"], 70 dayNamesShort: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], 71 dayNamesMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], 72 weekHeader: "KW", 73 dateFormat: "dd/mm/yy", 74 firstDay: 1, 75 isRTL: !1, 76 showMonthAfterYear: !1, 77 yearSuffix: "" 78 }, 79 e.datepicker.regional.es = 80 { 81 closeText: "Cerrar", 82 prevText: "<Ant", 83 nextText: "Sig>", 84 currentText: "Hoy", 85 monthNames: ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"], 86 monthNamesShort: ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"], 87 dayNames: ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"], 88 dayNamesShort: ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"], 89 dayNamesMin: ["D", "L", "M", "X", "J", "V", "S"], 90 weekHeader: "Sm", 91 dateFormat: "dd/mm/yy", 92 firstDay: 1, 93 isRTL: false, 94 showMonthAfterYear: false, 95 yearSuffix: "" 96 }, 97 e.datepicker.regional.it = 98 { 99 closeText: "Chiudi", 100 prevText: "<Prec", 101 nextText: "Succ>", 102 currentText: "Oggi", 103 monthNames: ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"], 104 monthNamesShort: ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"], 105 dayNames: ["Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"], 106 dayNamesShort: ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"], 107 dayNamesMin: ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"], 108 weekHeader: "Sm", 109 dateFormat: "dd/mm/yy", 110 firstDay: 1, 111 isRTL: false, 112 showMonthAfterYear: false, 113 yearSuffix: "" 114 }, 115 e.datepicker.regional.ca = 116 { 117 closeText: "Tanca", 118 prevText: "Anterior", 119 nextText: "Següent", 120 currentText: "Avui", 121 monthNames: ["gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"], 122 monthNamesShort: ["gen", "feb", "març", "abr", "maig", "juny", "jul", "ag", "set", "oct", "nov", "des"], 123 dayNames: ["diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"], 124 dayNamesShort: ["dg", "dl", "dt", "dc", "dj", "dv", "ds"], 125 dayNamesMin: ["dg", "dl", "dt", "dc", "dj", "dv", "ds"], 126 weekHeader: "Set", 127 dateFormat: "dd/mm/yy", 128 firstDay: 1, 129 isRTL: false, 130 showMonthAfterYear: false, 131 yearSuffix: "" 132 }, 133 e.datepicker.regional.ru = 134 { 135 closeText: "ÐакÑÑÑÑ", 136 prevText: "<ÐÑед", 137 nextText: "След>", 138 currentText: "СегоднÑ", 139 monthNames: ["ЯнваÑÑ", "ФевÑалÑ", "ÐаÑÑ", "ÐпÑелÑ", "Ðай", "ÐÑнÑ", "ÐÑлÑ", "ÐвгÑÑÑ", "СенÑÑбÑÑ", "ÐкÑÑбÑÑ", "ÐоÑбÑÑ", "ÐекабÑÑ"], 140 monthNamesShort: ["Янв", "Фев", "ÐаÑ", "ÐпÑ", "Ðай", "ÐÑн", "ÐÑл", "Ðвг", "Сен", "ÐкÑ", "ÐоÑ", "Ðек"], 141 dayNames: ["воÑкÑеÑенÑе", "понеделÑник", "вÑоÑник", "ÑÑеда", "ÑеÑвеÑг", "пÑÑниÑа", "ÑÑббоÑа"], 142 dayNamesShort: ["вÑк", "пнд", "вÑÑ", "ÑÑд", "ÑÑв", "пÑн", "ÑбÑ"], 143 dayNamesMin: ["ÐÑ", "Ðн", "ÐÑ", "СÑ", "ЧÑ", "ÐÑ", "Сб"], 144 weekHeader: "Ðед", 145 dateFormat: "dd.mm.yy", 146 firstDay: 1, 147 isRTL: false, 148 showMonthAfterYear: false, 149 yearSuffix: "" 150 }, 151 e.datepicker.regional.zh = 152 { 153 closeText: "å ³é", 154 prevText: "<䏿", 155 nextText: "䏿>", 156 currentText: "ä»å¤©", 157 monthNames: ["䏿", "äºæ", "䏿", "åæ", "äºæ", "å æ", "䏿", "å «æ", "乿", "åæ", "å䏿", "åäºæ"], 158 monthNamesShort: ["䏿", "äºæ", "䏿", "åæ", "äºæ", "å æ", "䏿", "å «æ", "乿", "åæ", "å䏿", "åäºæ"], 159 dayNames: ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå "], 160 dayNamesShort: ["卿¥", "å¨ä¸", "å¨äº", "å¨ä¸", "å¨å", "å¨äº", "å¨å "], 161 dayNamesMin: ["æ¥", "ä¸", "äº", "ä¸", "å", "äº", "å "], 162 weekHeader: "å¨", 163 dateFormat: "yy-mm-dd", 164 firstDay: 1, 165 isRTL: false, 166 showMonthAfterYear: true, 167 yearSuffix: "å¹´" 168 }, 169 e.datepicker.regional.pt = { 170 closeText: "Fechar", 171 prevText: "Anterior", 172 nextText: "Seguinte", 173 currentText: "Hoje", 174 monthNames: ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", 175 "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"], 176 monthNamesShort: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"], 177 dayNames: ["Domingo", "Segunda-feira", "Terça-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "SÃ
177¡bado"], 178 dayNamesShort: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"], 179 dayNamesMin: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"], 180 weekHeader: "Sem", 181 dateFormat: "dd/mm/yy", 182 firstDay: 1, 183 isRTL: false, 184 showMonthAfterYear: false, 185 yearSuffix: "" 186 }, 187 e.datepicker.regional.pl = { 188 closeText: "Zamknij", 189 prevText: "<Poprzedni", 190 nextText: "NastÄpny>", 191 currentText: "DziÅ", 192 monthNames: ["StyczeÅ", "Luty", "Marzec", "KwiecieÅ", "Maj", "Czerwiec", "Lipiec", "SierpieÅ", "WrzesieÅ", "Październik", "Listopad", "GrudzieÅ"], 193 monthNamesShort: ["Sty", "Lu", "Mar", "Kw", "Maj", "Cze", 194 "Lip", "Sie", "Wrz", "Pa", "Lis", "Gru"], 195 dayNames: ["Niedziela", "PoniedziaÅek", "Wtorek", "Åroda", "Czwartek", "PiÄ tek", "Sobota"], 196 dayNamesShort: ["Nie", "Pn", "Wt", "År", "Czw", "Pt", "So"], 197 dayNamesMin: ["N", "Pn", "Wt", "År", "Cz", "Pt", "So"], 198 weekHeader: "Tydz", 199 dateFormat: "dd.mm.yy", 200 firstDay: 1, 201 isRTL: false, 202 showMonthAfterYear: false, 203 yearSuffix: "" 204 }, 205 e.datepicker.regional.ja = { 206 closeText: "éãã", 207 prevText: "<å", 208 nextText: "次>", 209 currentText: "仿¥", 210 monthNames: ["1æ", "2æ", "3æ", "4æ", "5æ", "6æ", "7æ", "8æ", "9æ", "10æ", "11æ", "12æ"], 211 monthNamesShort: ["1æ", "2æ", "3æ", "4æ", "5æ", "6æ", "7æ", "8æ", "9æ", "10æ", "11æ", "12æ"], 212 dayNames: ["æ¥ææ¥", "æææ¥", "ç«ææ¥", "æ°´ææ¥", "æ¨ææ¥", "éææ¥", "åææ¥"], 213 dayNamesShort: ["æ¥", "æ", "ç«", "æ°´", "æ¨", "é", "å"], 214 dayNamesMin: ["æ¥", "æ", "ç«", "æ°´", "æ¨", "é", "å"], 215 weekHeader: "é±", 216 dateFormat: "yy/mm/dd", 217 firstDay: 0, 218 isRTL: false, 219 showMonthAfterYear: true, 220 yearSuffix: "å¹´" 221 }; 222}); 223 224/************************************************ 225 Convert price 226 ************************************************/ 227function convertPrice(price) { 228 return price.toLocaleString(language, {minimumFractionDigits: 2, style: 'currency', currency: currency}); 229} 230 231/************************************************ 232 Personal Details 233 ************************************************/ 234const generalError = document.querySelector('.error-container[role="alertdialog"]'); 235if (generalError) { 236 generalError.setAttribute('tabindex', '0'); 237 generalError.focus(); 238} 239 240document.addEventListener("DOMContentLoaded", function () { 241 const firstError = document.querySelector('.error-input span'); 242 if (firstError) { 243 firstError.focus(); 244 } 245 246 document.querySelectorAll('input, select, textarea').forEach(element => { 247 element.addEventListener('input', function() { 248 const parentLi = this.closest('li'); 249 if (parentLi) { 250 this.classList.remove('error'); 251 252 const errorDiv = parentLi.querySelector('.error-input'); 253 if (errorDiv) errorDiv.style.display = 'none'; 254 } 255 }); 256 }); 257 258 document.querySelectorAll('ul[role="radiogroup"]').forEach(group => { 259 group.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(radio => { 260 radio.addEventListener('change', function() { 261 group.querySelectorAll('input').forEach(input => input.classList.remove('error')); 262 const groupError = group.closest('li').querySelector('.error-input'); 263 if (groupError) groupError.style.display = 'none'; 264 }); 265 }); 266 }); 267 268 setTimeout(function() { 269 // Get translations and elements 270 const translations = document.getElementById('country-code-translations'); 271 const selectionText = translations?.getAttribute('data-country-code-selection') || 'Select country code'; 272 const selectedText = translations?.getAttribute('data-country-code-selected') || 'Country code is selected'; 273 const countryList = document.querySelector('.iti__country-list'); 274 275 if (!countryList) return; 276 277 // Single function to handle all accessibility enhancements 278 function makeAccessible() { 279 // Make the list accessible 280 countryList.setAttribute('aria-label', selectionText); 281 countryList.setAttribute('title', selectionText); 282 283 // Make the flag accessible 284 const flag = document.querySelector('.iti__selected-flag'); 285 if (flag) { 286 const dialCode = flag.getAttribute('title')?.match(/:\s*(\+\d+)$/)?.[1] || '';
287 flag.setAttribute('aria-label', selectionText + dialCode); 288 flag.setAttribute('title', selectionText + dialCode); 289 } 290 291 // Make each country option accessible 292 document.querySelectorAll('.iti__country').forEach(country => { 293 const dialCode = country.querySelector('.iti__dial-code')?.textContent || ''; 294 const isSelected = country.classList.contains('iti__highlight') || 295 country.classList.contains('iti__active'); 296 297 // Set appropriate text and attributes 298 country.setAttribute('aria-label', isSelected ? dialCode + selectedText : dialCode); 299 country.setAttribute('title', isSelected ? dialCode + selectedText : dialCode); 300 country.setAttribute('aria-selected', isSelected ? 'true' : 'false'); 301 302 // Hide redundant text from screen readers 303 country.querySelector('.iti__country-name')?.setAttribute('aria-hidden', 'true'); 304 }); 305 } 306 307 // Run initially 308 makeAccessible(); 309 310 // Watch for changes 311 new MutationObserver(makeAccessible).observe(countryList, { 312 attributes: true, attributeFilter: ['class'], subtree: true 313 }); 314 315 // Handle keyboard navigation 316 document.addEventListener('keydown', function(e) { 317 if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && 318 !countryList.classList.contains('iti__hide')) { 319 setTimeout(makeAccessible, 50); 320 } 321 }); 322 }, 500); 323 324}); 325 326/************************************************ 327 checkDateWithoutTimeslots 328 ************************************************/ 329function checkDateWithoutTimeslots(date) { 330 var date = new Date(date), 331 yy = date.getFullYear(), 332 mm = ('0' + (date.getMonth() + 1)).slice(-2), 333 dd = ('0' + date.getDate()).slice(-2), 334 weekday = date.getDay(), 335 dates = dateWithoutTimeslots['dates'] === undefined ? [] : dateWithoutTimeslots['dates'], 336 weekdays = dateWithoutTimeslots['weekdays'] === undefined ? [] : dateWithoutTimeslots['weekdays'], 337 period = dateWithoutTimeslots['period'] === undefined ? [] : dateWithoutTimeslots['period'], 338 validDates = false, 339 validWeekdays = false, 340 validPeriod = false; 341 342 if (dates.indexOf(yy + '-' + mm + '-' + dd) > -1) { 343 validDates = true; 344 } 345 346 if (weekdays.indexOf(weekday) > -1) { 347 validWeekdays = true; 348 } 349 350 if (period.length) { 351 period.forEach(function (period) { 352 var start = new Date(period.start), 353 end = new Date(period.end); 354 355 if (date.getTime() >= start.getTime() && date.getTime() <= end.getTime()) { 356 validPeriod = true; 357 } 358 }); 359 } 360 361 return (validDates || validWeekdays || validPeriod); 362} 363 364$(function () { 365 /************************************************ 366 navigation bar 367 ************************************************/ 368 // disable hyperlinks if it does not have class prev 369 $('.nav-bar a').on('click', function (e) { 370 if (!$(this).parent().hasClass('prev')) { 371 e.preventDefault(); 372 } 373 }); 374 375 /************************************************ 376 disable spacebar on input email 377 ************************************************/ 378 $('input[type="email"]').on('keydown', function (e) { 379 if (e.which === 32) { 380 return false; 381 } 382 }); 383 384 /************************************************ 385 Set product data in html 386 ************************************************/ 387 // number of selected products 388 function selectedVal(container) { 389 var total = 0; 390 var hasSubtotal = 'has-subtotal'; 391 392 container.find('.row').not('.show-all-rows').each(function () { 393 if ($(this).find('select').length > 0) { 394 var number = parseInt($(this).find('select').val()); 395 396 total += number; 397 } 398 }); 399 400 container.attr('data-quantity', total).data('quantity', total); 401 if (total > 0) { 402 container.addClass(hasSubtotal); 403 container.find('.last-row').show(); 404 showDonationSection(); 405 } else { 406 container.removeClass(hasSubtotal); 407 container.find('.last-row').hide(); 408 hideDonationSection(); 409 } 410 } 411 412 // calculate difference price 413 function differencePrice(container) { 414 var difference = 0; 415 416 container.find('.row').each(function () { 417 if ($(this).find('select').length > 0) { 418 var price = parseFloat($(this).find('select').data('difference')); 419 var number = parseFloat($(this).find('select').val()); 420 var rowPrice = price * number; 421 422 $(this).attr('data-row-difference', rowPrice).data('row-difference', rowPrice); 423 424 difference += rowPrice; 425 } 426 }); 427 428 return difference; 429 } 430 431 // calculate price per row 432 function subtotal(container) { 433 var subtotal = 0; 434 435 container.find('.row').not('.soldout, .show-all-rows').each(function () { 436 if ($(this).find('select').length > 0) { 437 var price = parseFloat($(this).find('select').data('price')); 438 var number = parseFloat($(this).find('select').val()); 439 var rowPrice = price * number; 440 441 $(this).attr('data-row-price', rowPrice).data('row-price', rowPrice); 442 $(this).find('.subtotal span').html(convertPrice(rowPrice)); 443 444 // show price if a product is selected 445 number > 0 ? $(this).find('.subtotal span').show() : $(this).find('.subtotal span').hide(); 446 447 subtotal += rowPrice; 448 } 449 }); 450 451 return subtotal; 452 } 453 454 // calculate discount 455 function discountData(container) { 456 var inputCode = $('#input-code'); 457 458 if (inputCode.length == 0) { 459 return; 460 } 461 462 var discountCode = inputCode.val().trim(); 463 464 var tickets = {}; 465 var ticketEvents = $('html').hasClass('ticketEvents'); 466 var hasDiscount = 'has-discount'; 467 468 container.find('.selectbox select[name^="tickets"], .selectbox select[name^="subscription"]').each(function () { 469 var id = $(this).attr('id'); 470 id = id.replace('tickettype_', ''); 471 id = id.replace('subscriptiontype_', ''); 472 473 tickets[id] = parseInt($(this).val()); 474 }); 475 476 /* Check if the input field has been filled in */ 477 if (discountCode.length > 0) { 478 /* ajax call */ 479 $.ajax({ 480 url: '/script/discount', 481 data: { 482 eventId: eventId, 483 discountCode: discountCode, 484 tickets: tickets, 485 ticketEvents: ticketEvents, 486 streetname: streetname 487 }, 488 cache: false, 489 dataType: 'json', 490 method: 'POST', 491 success: function (output) { 492 if (output.success) { 493 var discountPrice = output.discountPrice, 494 subTotal = output.subTotal; 495 496 var total = (subTotal - discountPrice); 497 498 // hide error elements 499 $('.code-container, #discount-error').hide(); 500 $('#discount-error').attr('aria-hidden', 'true').hide(); 501 502 // show discount elements 503 $('.discount-container').show(); 504 $('.subtotal-price').show(); 505 $('#discount-success').show(); 506 507 // set values 508 $('.discount-id').html(discountCode); 509 $('.discount-val').html('- ' + convertPrice(discountPrice)); 510 $('#subtotal-price').html(convertPrice(subTotal)); 511 $('.total-price').html(convertPrice(total)); 512 ticketsTotalPrice = total; 513 514 var initialPrice = getInitialDonationPrice(); 515 516 calcDonation(initialPrice); 517 518 // has discount 519 container.addClass(hasDiscount); 520 } else { 521 $('.subtotal-price, .discount-container').hide(); 522 $('#discount-error').attr('aria-hidden', 'false').show(); 523 $('#discount-error, .code-container').show(); 524 $('#input-code').addClass('error'); 525 526 // no discount 527 container.removeClass(hasDiscount); 528 } 529 } 530 }); 531 } 532 } 533 534 535 /************************************************ 536 Total 537 ************************************************/ 538 // contain every calculating functions 539 function rowContainerFunctions() { 540 var totalPrice = 0; 541 542 $('.row-container').each(function () { 543 selectedVal($(this)); 544 545 var differenceVal = differencePrice($(this)); 546 var subtotalVal = subtotal($(this)); 547 var discountVal = discountData($(this)); 548 var calcArray = []; 549 550 // check subtotal 551 if ($(this).hasClass('has-subtotal')) { 552 calcArray.push(subtotalVal); 553 } 554 // check discount 555 /* 556 if ($(this).hasClass('has-discount')) 557 { 558 calcArray.push(discountVal); 559 } 560 */ 561 562 // calculate total price 563 $.each(calcArray, function (i, val) { 564 totalPrice += val; 565 }); 566 567 differenceVal > 0 ? $(this).find('.difference-row').fadeIn() : $(this).find('.difference-row').fadeOut(); 568 $(this).find('.total-difference').html(convertPrice(differenceVal)); 569 if (!$(this).hasClass('has-discount')) { 570 $(this).find('.total-price').html(convertPrice(totalPrice)); 571 ticketsTotalPrice = totalPrice; 572 573 var initialPrice = getInitialDonationPrice(); 574 575 calcDonation(initialPrice); 576 } 577 }); 578 } 579 580 // initiate functions 581 // select products 582 $('.row-container select').on('change selectmenuchange', function () { 583 var event = $(this).data('event'), 584 allowOne = $(this).closest('.row-container').hasClass('allow-one'), 585 ignoreEvent = $(this).closest('.row-container').hasClass('ignore-event'); 586 587 if (event != undefined && !ignoreEvent) { 588 var selects = $('.row-container select').not('[data-event="' + event + '"]'); 589 590 selects.val(0); 591 if (selects.hasClass('gt-desktop-select')) { 592 selects.selectmenu('refresh'); 593 } 594 } 595 596 if (allowOne) { 597 var selects = $('.row-container select').not($(this)); 598 599 selects.val(0); 600 601 if (selects.hasClass('gt-desktop-select')) { 602 selects.selectmenu('refresh'); 603 } 604 } 605 606 rowContainerFunctions(); 607 }); 608 609 //show more option 610 $('.row-container .show-all-rows').on('click', function (event) { 611 event.preventDefault(); 612 $(this).hide().closest('.row-container').find('.extra-row, .event-title').show(); 613 }) 614 $('.row-container .show-ticket-types-in-ticket-group').on('click', function (event) { 615 event.preventDefault(); 616 $(this).hide().prevUntil('.event-title').show(); 617 }) 618 619 // select discount 620 $('#submit-input-code').on('click', function (event) { 621 rowContainerFunctions() 622 event.preventDefault(); 623 624 setTimeout(() => { 625 const $error = $('#discount-error'); 626 if ($error.length > 0) { 627 const $errorSpan = $error.find('span'); 628 if ($errorSpan.length > 0 && $error.is(':visible')) { 629 const html = $errorSpan.html(); 630 if (html) { 631 const cleanText = html.replace(/<br>\n?/gi, ' '); 632 $errorSpan.attr('aria-label', cleanText); 633 $error.attr('aria-hidden', 'false').trigger('focus'); 634 } 635 } 636 } 637 }, 500); 638 }); 639 640 $('#input-code').on('keydown', function (event) { 641 var key = event.keyCode || event.which; 642 643 if (key === 13) { 644 rowContainerFunctions() 645 event.preventDefault(); 646 } 647 }); 648 649 /************************************************ 650 Row-container description 651 ************************************************/ 652 $('.col.type a').on('click', function (e) { 653 e.preventDefault(); 654 655 $(this).closest('.row').find('.description').toggle('fade'); 656 }); 657 658 /************************************************ 659 Discount code 660 ************************************************/ 661 $('#code-button').on('click', function (e) { 662 $(this).closest('.discount').addClass('active').find('input').focus(); 663 e.preventDefault(); 664 }); 665 666 /************************************************ 667 jQuery UI: Dialogs 668 ************************************************/ 669 // Close dialog button 670 $(document).on('click', '.ui-widget-overlay.ui-front, .close-dialog-button, .ui-dialog, .close-dialog-tag', function (e) { 671 e.preventDefault(); 672 $('.ui-dialog-content').not('.disable-close-dialog').dialog('close'); 673 }); 674 675 $(document).on('click', '.dialog-content, .dialog-header, .dialog-footer', function (e) { 676 e.stopPropagation(); 677 }); 678 679 // faq dialog 680 $('#faq-dialog').dialog({ 681 autoOpen: false, 682 modal: true, 683 width: '100%', 684 resizable: false, 685 draggable: false, 686 show: {effect: "puff", duration: 300}, 687 hide: {effect: "puff", duration: 300} 688 }); 689 690 $('#faq-button, .faq-button, [href="#faq"]').not('.external').on('click', function (e) { 691 $('#faq-dialog').dialog('open'); 692 693 e.preventDefault(); 694 }); 695 696 $('#faq-dialog .dialog-wrapper').on('click', function (e) { 697 e.stopPropagation(); 698 }); 699 $('.close-dialog-button').on('click', function (e) { 700 $('#faq-dialog').dialog('close'); 701 e.preventDefault(); 702 }); 703 704 /***** .message-dialog *****/ 705 $('.message-dialog').dialog( 706 { 707 autoOpen: false, 708 modal: true, 709 draggable: false, 710 resizable: false, 711 width: '100%', 712 }); 713 714 /***** [data-dialog] *****/ 715 $('[data-dialog]').on('click', function (e) { 716 e.preventDefault(); 717 718 var dialog = $(this).data('dialog'); 719 720 $(dialog).dialog('open'); 721 }); 722 723 724 /************************************************ 725 Datepicker: Calendar 726 ************************************************/ 727 // datepicker localisation 728 $.datepicker.setDefaults($.datepicker.regional[language]); 729 730 function checkDisabledDates(calendarDate) { 731 calendarDate.setHours(0, 0, 0, 0); 732 733 var calendarWeekDay = calendarDate.getDay(); 734 735 var dateTitle = '', 736 dateClassAttr = ''; 737 738 if (datesTitle != undefined) { 739 var weekDays = datesTitle.weekDays, 740 dates = datesTitle.dates, 741 upcomingDays = datesTitle.upcomingDays; 742 743 dateTitle = datesTitle.default != undefined ? datesTitle.default.title : ''; 744 dateClassAttr = datesTitle.default != undefined ? datesTitle.default.classAttr : ''; 745
746 $.each(weekDays, function (i, data) { 747 var title = data.title != undefined && data.title.length > 0, 748 classAttr = data.classAttr != undefined && data.classAttr.length > 0; 749 750 if (calendarDate.getDay() == i) { 751 dateTitle = title; 752 dateClassAttr = classAttr; 753 } 754 }); 755 756 $.each(dates, function (i, data) { 757 var d = new Date(data.date), 758 title = data.title, 759 classAttr = data.classAttr; 760 761 d.setHours(0, 0, 0, 0); 762 calendarDate.setHours(0, 0, 0, 0); 763 764 if (d.getTime() == calendarDate.getTime()) { 765 dateTitle = title; 766 dateClassAttr = classAttr 767 } 768 }); 769 770 $.each(upcomingDays, function (i, data) { 771 var d = new Date(), 772 title = data.title, 773 classAttr = data.classAttr; 774 775 d.setDate(d.getDate() + i); 776 d.setHours(0, 0, 0, 0); 777 calendarDate.setHours(0, 0, 0, 0); 778 779 if (d.getTime() == calendarDate.getTime()) { 780 dateTitle = title; 781 dateClassAttr = classAttr 782 } 783 }); 784 } 785 786 var month = ("0" + (calendarDate.getMonth() + 1)).slice(-2); 787 788 var d = calendarDate.getFullYear() + '-' + month + '-' + ("0" + calendarDate.getDate()).slice(-2); 789 790 if (disabledDatesTitle === false && (ticketMinDate > calendarDate || ticketMaxDate < calendarDate)) { 791 return [false, 'disabled']; 792 } 793 794 // Disable the date is the date is marked as one of the closing days. 795 if(typeof disabledDates !== 'undefined') { 796 const year = calendarDate.getFullYear();
797 const month = String(calendarDate.getMonth() + 1).padStart(2, '0'); // Months are 0-based, so add 1 798 const day = String(calendarDate.getDate()).padStart(2, '0'); // Ensure day is always 2 digits 799 800 if(disabledDates.includes(`${year}-${month}-${day}`)) { 801 return [false, 'disabled']; 802 } 803 } 804 805 if ($.inArray(calendarDate.getDay(), disabledWeekDays) >= 0 || $.inArray(d, disabledDates) >= 0) { 806 if ($.inArray(d, openDates) < 0) { 807 return [false, 'disabled', (disabledDatesTitle === true ? dateTitle : '')]; 808 } 809 } 810 811 if ($.inArray(d, soldoutDates) >= 0) { 812 return [false, 'disabled soldout', '']; 813 } 814 815 let result = [true, dateClassAttr, dateTitle]; 816 817 if(typeof window.customCheckDisabledDatesEvent != 'undefined') { 818 result = window.customCheckDisabledDatesEvent(calendarDate, result); 819 } 820 821 return result; 822 } 823 824 var currentRequest = null; 825 826 // create datepicker inline (calendar) 827 $('.calendar').datepicker({ 828 altField: '#calendar-date', 829 beforeShowDay: checkDisabledDates, 830 dateFormat: 'yy-mm-dd', 831 firstDay: 1, 832 minDate: ticketMinDate, 833 maxDate: ticketMaxDate, 834 onSelect: function (ticketDate) { 835 $('.ui-datepicker').removeClass('no-default-date'); 836 837 // skip timeslots if the selected date has no timeslots 838 if ( 839 settingsTimeslots === false 840 || (typeof dateWithoutTimeslots !== 'undefined' && checkDateWithoutTimeslots(ticketDate)) 841 ) { 842 $('#timeslot-section .timeslots-container input:checked').prop('checked', false); 843 $('#timeslot-section').removeClass('completed').slide
843Up().find('.error-container').hide(); 844 if (hasTicketsSelected()) { 845 showNextButton(); 846 } 847 } else { 848 ticketDateChosen(ticketDate); 849 } 850 } 851 }); 852 853 // update calendar and get timeslots based on chosenTicketDate 854 function setDatepickerDate() { 855 $('.calendar').datepicker('setDate', null); 856 857 if (!selectDateOnPageLoad && !chosenTicketDate) { 858 $('.ui-datepicker') 859 .addClass('no-default-date') 860 .find('.ui-state-active, .ui-state-highlight, .ui-datepicker-current-day, .ui-state-hover') 861 .removeClass('ui-state-active ui-state-highlight ui-datepicker-current-day ui-state-hover'); 862 863 return; 864 } 865 866 const today = new Date(); 867 const yy = today.getFullYear(); 868 const mm = ('0' + (today.getMonth() + 1)).slice(-2); 869 const dd = ('0' + today.getDate()).slice(-2); 870 const ticketDate = yy + '-' + mm + '-' + dd; 871 872 var setDate = chosenTicketDate ? chosenTicketDate : ticketDate; 873 var dateIsValid = checkDisabledDates(new Date(setDate))[0]; 874 875 if (dateIsValid) { 876 $('.calendar').datepicker('setDate', setDate); 877 } 878 879 if (typeof dateWithoutTimeslots !== 'undefined' && checkDateWithoutTimeslots(chosenTicketDate)) { 880 $('#timeslot-section .timeslots-container input:checked').prop('checked', false); 881 $('#timeslot-section').removeClass('completed').hide(); 882 showNextButton(); 883 } else if (dateIsValid) { 884 ticketDateChosen(setDate); 885 } 886 } 887 888 if ( 889 page === 'date' 890 && !$('#gift-ticket').prop('checked') 891 && ( 892 chosenTicketDate !== false 893 || $('.calendar').datepicker('getDate') 894 ) 895 ) { 896 setDatepickerDate(); 897 } 898 899 // if only one date is available 900 if ( 901 page === 'date' 902 && !$('#gift-ticket').prop('checked') 903 && ticketMinDate.getTime() === ticketMaxDate.getTime() 904 ) { 905 var yy = ticketMinDate.getFullYear(), 906 mm = ('0' + (ticketMinDate.getMonth() + 1)).slice(-2), 907 dd = ('0' + ticketMinDate.getDate()).slice(-2), 908 ticketDate = yy + '-' + mm + '-' + dd; 909 910 $('.calendar').datepicker('setDate', ticketDate); 911 $('#calendar-date').val(ticketDate); 912 913 ticketDateChosen(ticketDate); 914 } 915 916 if (isAccessibleDatepicker) { 917 const datepickerConfig = { 918 inline: true, 919 monthSelectorType: 'static', 920 yearSelectorType: 'static', 921 minDate: ticketMinDate, 922 maxDate: ticketMaxDate, 923 altFormat: 'l j F Y', 924 ariaDateFormat: 'l j F Y', 925 altInput: true, 926 altInputClass: 'alt-datepicker', 927 disable: [ 928 function (calendarDate) { 929 var calendarWeekDay = calendarDate.getDay(); 930 var month = ("0" + (calendarDate.getMonth() + 1)).slice(-2); 931 var d = calendarDate.getFullYear() + '-' + month + '-' + ("0" + calendarDate.getDate()).slice(-2); 932 933 if ($.inArray(calendarWeekDay, disabledWeekDays) >= 0 || $.inArray(d, disabledDates) >= 0) { 934 if ($.inArray(d, openDates) < 0) { 935 return true; 936 } 937 } 938 939 return false; 940 } 941 ], 942 onChange: function (selectedDates, dateStr, instance) { 943 const formattedDate = instance.formatDate(selectedDates[0], 'l j F Y'); 944 ticketDateChosen(dateStr, formattedDate); 945 }, 946 947 locale: { 948 weekdays: { 949 shorthand: ["zo", "ma", "di", "wo", "do", "vr", "za"], 950 longhand: [ 951 "zondag", 952 "maandag", 953 "dinsdag", 954 "woensdag", 955 "donderdag", 956 "vrijdag", 957 "zaterdag", 958 ], 959 }, 960 961 months: { 962 shorthand: [ 963 "jan", 964 "feb", 965 "mrt", 966 "apr", 967 "mei", 968 "jun", 969 "jul", 970 "aug", 971 "sept", 972 "okt", 973 "nov", 974 "dec", 975 ], 976 longhand: [ 977 "januari", 978 "februari", 979 "maart", 980 "april", 981 "mei", 982 "juni", 983 "juli", 984 "augustus", 985 "september", 986 "oktober", 987 "november", 988 "december", 989 ], 990 }, 991 992 firstDayOfWeek: 1, 993 weekAbbreviation: "wk", 994 rangeSeparator: " t/m ", 995 scrollTitle: "Scroll voor volgende / vorige", 996 toggleTitle: "Klik om te wisselen", 997 time_24hr: true, 998 999 ordinal: function (nth) { 1000 if (nth === 1 || nth === 8 || nth >= 20) return "ste"; 1001 1002 return "de"; 1003 }, 1004 } 1005 }; 1006
1007 $("#calendar-date").flatpickr(datepickerConfig); 1008 } 1009 1010 /************************************************ 1011 Personal Data page: Confirm email 1012 ************************************************/ 1013 1014 /* 1015 * function to create the email address confirmation placeholder 1016 */ 1017 function createEmailAddressPlaceholder() { 1018 /* variable */ 1019 var email = '#emailAddress'; 1020 var emailConfirm = '#emailAddressConfirm'; 1021 var emailAddress = $(email).val(); 1022 var emailAddressConfirm = $(emailConfirm).val(); 1023 var placeholderText = ''; 1024 1025 /* check for value */ 1026 if (emailAddress.length > 0) { 1027 var error = false; 1028 var emailAddressArray = emailAddress.split(''); 1029 var emailAddressConfirmArray = emailAddressConfirm.split(''); 1030 1031 /* placeholder */ 1032 placeholderText = LANG_REPEAT + ': ' + emailAddress; 1033 1034 /* compare email address length */ 1035 if (emailAddressConfirm.length > emailAddress.length) { 1036 error = true; 1037 } else { 1038 /* read array */ 1039 $.each(emailAddressArray, function (index, value) { 1040 if (emailAddressConfirmArray[index] && value != emailAddressConfirmArray[index]) { 1041 error = true; 1042 } 1043 }); 1044 } 1045 } 1046 1047 /* check if the email addresses do match */ 1048 if (error) { 1049 $(emailConfirm).addClass('error'); 1050 } else { 1051 $(emailConfirm).removeClass('error'); 1052 } 1053 } 1054 1055 /* 1056 * email address handlers 1057 */ 1058 $('#emailAddress, #emailAddressConfirm').on('keyup change', function () { 1059 createEmailAddressPlaceholder(); 1060 }); 1061 1062 /* 1063 * Disable paste 1064 */ 1065 $('#emailAddressConfirm').on('paste', function (e) { 1066 e.preventDefault(); 1067 }); 1068 1069 1070 /************************************************ 1071 Payment page: #more-payment 1072 ************************************************/ 1073 // display payment options 1074 $('#more-payments').on('click', function (e) { 1075 e.preventDefault(); 1076 $(this).fadeOut(); 1077 $('#payment-section li').fadeIn(); 1078 1079 const $paymentMethods = $('#payment-section li input'); 1080 1081 if($paymentMethods.length > 0) { 1082 $paymentMethods.first().focus(); 1083 } 1084 }); 1085 1086 /************************************************ 1087 selected paymentmethod 1088 ************************************************/ 1089 if (typeof CMPayment !== 'undefined' && CMPayment !== false) { 1090 $('#payment-section input').on('change', function () { 1091 var method = $(this).val(), 1092 isCMPayment = CMPayment.indexOf(method) > -1; 1093 1094 $('#payment-forms > div').removeClass('active'); 1095 1096 if ( 1097 isCMPayment === false 1098 || $('#' + method + '-form').length === 0 1099 ) { 1100 $('#payment-forms').removeClass('active required'); 1101 return; 1102 } 1103 1104 $('#payment-forms').addClass('active required'); 1105 $('#' + method + '-form').addClass('active'); 1106 }); 1107 1108 /***** #ideal-form *****/ 1109 $('#ideal-buttons input').on('change', function () { 1110 var value = $(this).attr('id'); 1111 1112 $('#ideal-banks').val(value); 1113 1114 if ($('#ideal-banks').hasClass('gt-desktop-select')) { 1115 $('#ideal-banks').selectmenu('refresh'); 1116 } 1117 1118 $('#payment-forms').addClass('completed'); 1119 displayNextButton(); 1120 }); 1121 1122 $('#ideal-banks').on('change selectmenuchange', function () { 1123 var value = $(this).val(), 1124 bank = $('#' + value); 1125 1126 if (bank.length === 0) { 1127 $('#ideal-buttons input:checked').prop('checked', false); 1128 } else { 1129 bank.prop('checked', true); 1130 $('#payment-forms').addClass('completed'); 1131 displayNextButton(); 1132 } 1133 }); 1134 } 1135 1136 displayNextButton(); 1137 1138 /************************************************ 1139 Ticket page: next button 1140 ************************************************/
1141 // return true or false if a ticket is selected 1142 function ticketCondition() { 1143 var section = $('#tickets-section'); 1144 var tickets = section.find('.row-container select').filter(function () { 1145 return $(this).val() > 0; 1146 }).length > 0; 1147 var condition = false; 1148 1149 condition = tickets; 1150 1151 // add class 'completed' for single-page. If section has class 'completed', display next section 1152 sectionCompleted(section, condition); 1153 } 1154 1155 // initiate functions 1156 $('#tickets-section .row-container select').on('change selectmenuselect', function () { 1157 ticketCondition(); 1158 displayNextButton(); 1159 }); 1160 1161 // initiate functions 1162 $('#timeslot-section').on('change', '.timeslot input', function () { 1163 timeslotCondition(); 1164 displayNextButton(); 1165 }); 1166 1167 1168 /************************************************ 1169 Personal Data page: next button 1170 ************************************************/ 1171 // check fields with class '.required' 1172 function dataFieldsRequired(section) { 1173 var fields = section.find('.data-fields .required:last'); 1174 1175 // check if one of the radio buttons is checked 1176 var radio = fields.find('[type="radio"]').length ? fields.find('[type="radio"]').is(':checked') : true; 1177 1178 // check if every text field's length is not 0 1179 var text = fields.find('input').not('[type="radio"]').filter(function () { 1180 var val = $(this).val(); 1181 return val.trim().length === 0; 1182 }).length === 0; 1183 1184 // check if select is not default 1185 var select = fields.find('select').filter(function () { 1186 return $(this).val() == 'default'; 1187 }).length === 0; 1188 1189 // check if orderLimit has been reached 1190 var orderLimitReached = section.find('.error-container.orderLimitReached').length > 0; 1191 1192 return (radio && text && select && !orderLimitReached); 1193 } 1194 1195 // return true or false if select, radio buttons and input fields have values. 1196 function personalDataCondition() { 1197 var section = $('#personal-data-section, #subscription-data-section'); 1198 var condition = dataFieldsRequired(section); 1199 1200 sectionCompleted(section, condition); 1201 } 1202 1203 // initiate functions 1204 if ($('#personal-data-section').length) { 1205 personalDataCondition(); 1206 displayNextButton(); 1207 } 1208 1209 $('#subscription-data-section input, #personal-data-section input, #personal-data-section select, #personal-data-section textarea').on('keyup change selectmenuselect', function () { 1210 personalDataCondition(); 1211 displayNextButton(); 1212 }); 1213 1214 if ($('#subscription-data-section').length > 0) { 1215 showNextButton(); 1216 } 1217 1218 /************************************************ 1219 Payment page: next button 1220 ************************************************/ 1221 // return true or false if "payment option" and "terms and conditions" are selected 1222 function paymentCondition() { 1223 var section = $('#payment-section'); 1224 var condition = section.find('input').is(':checked'); 1225 1226 sectionCompleted(section, condition); 1227 } 1228 1229 function termsCondition() { 1230 var section = $('#terms-and-conditions-section'); 1231 var condition = section.find('#terms-and-conditions').is(':checked'); 1232 1233 sectionCompleted(section, condition); 1234 } 1235 1236 // initiate functions 1237 $('#terms-and-conditions, #payment-section input').on('change', function () { 1238 paymentCondition(); 1239 termsCondition(); 1240 displayNextButton(); 1241 }); 1242 1243 if ($('#terms-and-conditions').length) { 1244 paymentCondition(); 1245 termsCondition(); 1246 displayNextButton(); 1247 } 1248 1249 /************************************************ 1250 jQuery Selectmenu 1251 ************************************************/ 1252 // if window is smaller than 768px and select has class 'gt-desktop-select', destroy "selectmenu" else create it 1253 function selectMenuWidth() { 1254 var element = $('.selectbox select'), 1255 selectClass = 'gt-desktop-select'; 1256 1257 if ($(window).width() <= 768 && element.hasClass(selectClass)) { 1258 element.selectmenu('destroy'); 1259 element.removeClass(selectClass); 1260 } else if ($(window).width() >= 768 && !element.hasClass(selectClass)) { 1261 element.selectmenu( 1262 { 1263 width: '100%', 1264 position: {my: "left top", at: "left bottom", collision: "fit"}, 1265 select: function () { 1266 displayNextButton(); 1267 }, 1268 open: function (event, ui) { 1269 selectMenuKeyUp(); 1270 } 1271 }); 1272 1273 element.addClass(selectClass); 1274 } 1275 } 1276
1277 selectMenuWidth(); 1278 1279 $(window).resize(function () { 1280 selectMenuWidth(); 1281 }); 1282 1283 /************************************************ 1284 jQuery Selectmenu: keyup 1285 ************************************************/ 1286 function selectMenuKeyUp() { 1287 $('.ui-selectmenu-button').on('keyup', function () { 1288 var element = $('.ui-selectmenu-open'), 1289 active = $('.ui-menu-item-wrapper.ui-state-active'), 1290 position = active.length ? active.parent().position().top - active.closest('.ui-menu').position().top : 0, 1291 height = element.height() / 2; 1292 1293 element.scrollTop(position - height); 1294 }); 1295 } 1296 1297 /************************************ 1298 Donation 1299 *************************************/ 1300 function getInitialDonationPrice() { 1301 var initialPrice = 0; 1302 var checkedPrice = +$('[name="donation-input"]:checked').val(); 1303 var otherPrice = +$('#donation').val(); 1304 var inputPrice = +$('[name="donation-input"]').val(); 1305 1306 if (!isNaN(checkedPrice)) { 1307 initialPrice = checkedPrice; 1308 } else if (!isNaN(otherPrice)) { 1309 initialPrice = otherPrice; 1310 } else if (!isNaN(inputPrice)) { 1311 initialPrice = inputPrice; 1312 } 1313 1314 return initialPrice; 1315 } 1316 1317 function calcDonation(donationPrice = 0) { 1318 if (isNaN(donationPrice) || donationPrice <= 0) { 1319 $('#donation-section .total-price').html(convertPrice(ticketsTotalPrice)); 1320 $('#donation-section .donation-price').fadeOut(); 1321 } else { 1322 $('#donation-section .total-price').html(convertPrice(ticketsTotalPrice + donationPrice)); 1323 $('#donation-section .donation-price').fadeIn(); 1324 } 1325 } 1326 1327 if ($('#donation-section').length) { 1328 var initialPrice = getInitialDonationPrice(); 1329 1330 calcDonation(initialPrice); 1331 1332 $('#donation-check').on('change', function () { 1333 var checked = $(this).is(':checked'); 1334 1335 $('#donation-section .hidden-container').fadeIn(checked); 1336 1337 const focusableElements = $('#donations-section, #donations-section li, #donations-section input, #donation'); 1338 if (checked) { 1339 focusableElements.attr('tabindex', '0'); 1340 $('#donation').removeAttr('tabindex'); 1341 } else { 1342 focusableElements.attr('tabindex', '-1'); 1343 } 1344 }); 1345 1346 $('[name="donation-input"]').on('keyup change selectmenuchange', function () { 1347 var value = +$(this).val(); 1348 var otherDonation = +$('#donation').val(); 1349 var donationPrice = value ?? (otherDonation ?? 0); 1350 1351 calcDonation(donationPrice); 1352 }); 1353 } 1354 1355 if ($('#donation-section, #donations-section').length) { 1356 $('.donation-amount').on('change', function () { 1357 var val = $(this).val(); 1358 1359 if (val === 'other') { 1360 $('.donationOther').show(); 1361 $('.donationOther').find('input').prop('disabled', false); 1362 } else { 1363 $('.donationOther').hide(); 1364 $('.donationOther').find('input').prop('disabled', true); 1365 } 1366 }); 1367 } 1368 1369 function showDonationSection() { 1370 $('#donation-text-section, #donation-section').show(); 1371 } 1372 1373 function hideDonationSection() { 1374 $('#donation-text-section, #donation-section').hide(); 1375 } 1376 1377 setTimeout(function() { 1378 const donationItems = $('#donation-buttons li'); 1379 if (donationItems.length === 0) { 1380 return; 1381 } 1382 1383 donationItems.each(function() { 1384 $(this).on('focus', function() { 1385 const radioInput = $(this).find('input[type="radio"]'); 1386 if (radioInput.length) { 1387 radioInput.prop('checked', true).trigger('change'); 1388 1389 if (radioInput.attr('id') === 'donation_other' || radioInput.val() === 'other') { 1390 $('.donationOther').show(); 1391 $('#donation').prop('disabled', false); 1392 } else { 1393 $('.donationOther').hide(); 1394 $('#donation').prop('disabled', true); 1395 } 1396 } 1397 1398 const activeLabel = $(this).find('.donation-button.active'); 1399 if (activeLabel.length) { 1400 activeLabel.click(); 1401 } 1402 }); 1403 1404 $(this).on('keydown', function(e) { 1405 if ((e.key === 'Enter' || e.key === ' ') && 1406 $(this).find('input#donation_other').length) { 1407 e.preventDefault(); 1408 $('#donation').trigger('focus'); 1409 } 1410 }); 1411 }); 1412 }, 1000); 1413 1414 /************************************ 1415 Gift tickets 1416 *************************************/ 1417 $('.gift-container input').on('change', function () { 1418 let dateSection = $('#calendar-section, #timeslot-section'), 1419 upsellSection = $('#custom-upsell'), 1420 upsellExists = typeof resetUpsell === 'function'; 1421 isGiftTicketChecked = $('#gift-ticket').is(':checked'); 1422 let selectedDateText = $('#selected-date-text'); 1423 1424 if(isGiftTicketChecked) {
1425 dateSection.slideUp(); 1426 1427 if (upsellExists) { 1428 resetUpsell(); 1429 upsellSection.slideUp(); 1430 selectedDateText.hide(); 1431 } 1432 } 1433 else { 1434 $('#calendar-section').slideDown(function () { 1435 timeslotCondition(); 1436 setDatepickerDate(); 1437 }); 1438 if (upsellExists) { 1439 upsellSection.slideDown(); 1440 selectedDateText.show(); 1441 } 1442 } 1443 1444 var ticketDate = $('#calendar-date').val(); 1445 1446 if (isGiftTicketChecked === false && ticketDate.length) { 1447 ticketDateChosen(ticketDate); 1448 } 1449 if(upsellExists) { 1450 updateCart(ticketDate); 1451 } 1452 1453 sectionCompleted(dateSection, isGiftTicketChecked); 1454 displayNextButton(); 1455 }); 1456 $('.gift-container input').trigger('change'); 1457 /************************************************ 1458 allow input with only numbers 1459 ************************************************/ 1460 $('input.day, input.month, input.year').on('click', function () { 1461 $(this).select(); 1462 }); 1463 $('input.day, input.month, input.year').on('keydown', function (e) { 1464 var key = e.charCode || e.keyCode || 0; 1465 // allow backspace, numbers and keypad numbers ONLY 1466 // tabs, home, end, period, and numpad decimal 1467 // arrow keys 1468 return ( 1469 key == 8 || 1470 key == 9 || 1471 (key >= 37 && key <= 40) || 1472 key == 110 || 1473 key == 190 || 1474 (key >= 48 && key <= 57) || 1475 (key >= 96 && key <= 105) 1476 ); 1477 }); 1478 1479 $('input.day, input.month, input.year').on('keyup', function (e) { 1480 var tab = parseInt($(this).attr('tabindex')) + 1, 1481 min = parseInt($(this).data('min')), 1482 max = parseInt($(this).data('max')), 1483 length = parseInt($(this).data('length')), 1484 key = e.charCode || e.keyCode || 0; 1485 1486 var value = parseInt($(this).val()); 1487 1488 // +1 or -1 with arrow keys 1489 if (key == 38 && value < max) { 1490 $(this).val(value + 1); 1491 } 1492 if (key == 40 && value > min) { 1493 $(this).val(value - 1); 1494 } 1495 1496 // if this value length is same as max length, and it's not up or down key 1497 if ($(this).val().length == length && (key < 37 || key > 40) && key != 9) { 1498 // if length is same as max length, go to next field 1499 $('[tabindex=' + tab + ']').focus().select(); 1500 } 1501 }); 1502 1503 function birthdateBlur(input) { 1504 var min = parseInt(input.data('min')), 1505 max = parseInt(input.data('max')), 1506 value = input.val(); 1507 1508 // if value is smaller than min, set min 1509 if (value < min) { 1510 input.val(min); 1511 } 1512 // if value is greater than max, set max 1513 if (value > max) { 1514 input.val(max); 1515 } 1516 1517 if (parseInt(input.val()) < 10) { 1518 input.val(('0' + input.val()).slice(-2)); 1519 } 1520 } 1521 1522 $('input.day, input.month, input.year').on('blur', function (e) { 1523 if ($('.birthdate').hasClass('required')) { 1524 birthdateBlur($(this)) 1525 } else { 1526 if ($(this).val().length) { 1527 birthdateBlur($(this)); 1528 } 1529 } 1530 }); 1531 1532 /************************************************ 1533 faq-list 1534 ************************************************/ 1535 $('.faq-list .toggle-question').on('click', function (e) { 1536 $(this).parent().toggleClass('active'); 1537 1538 e.preventDefault(); 1539 }); 1540 1541 /************************************************ 1542 language selection 1543 ************************************************/ 1544 function updateLanguageIcon(languageCode) { 1545 var selectButton = $('#language-selection .ui-selectmenu-button'); 1546 var flagImg = $('<span class="language-flag"><span class="flag-icon ' + languageCode.toLowerCase() + '"></span></span>'); 1547 selectButton.find(".ui-selectmenu-text").prepend(flagImg); 1548 1549 $('#language-selection .language-flag-mobile .flag-icon') 1550 .attr('class', 'flag-icon ' + languageCode.toLowerCase()); 1551 } 1552 1553 if ($('#language-selection').hasClass('language-flags')) { 1554 updateLanguageIcon($("#language-selection select").val()); 1555 } 1556 1557 $('#language-selection select').on('change selectmenuchange', function () { 1558 window.location.href = $(this).find('option:selected').data('url'); 1559 }); 1560 1561 /************************************************ 1562 Subscriptions: Profile picture 1563 ************************************************/ 1564 if ($('.upload-image-container').length) { 1565 Dropzone.autoDiscover = false;
1566 1567 var uploadProfile = new Dropzone('.upload-image-container', 1568 { 1569 autoProcessQueue: true, 1570 uploadMultiple: false, 1571 maxFiles: 1, 1572 url: '/script/profile-picture', 1573 hiddenInputContainer: '#profile-picture-container', 1574 previewsContainer: '#preview-profile-picture', 1575 acceptedFiles: 'image/jpeg,image/png,image/gif', 1576 thumbnailWidth: 80, 1577 thumbnailHeight: 100 1578 }); 1579 1580 uploadProfile.on('addedfile', function (file) { 1581 // if another file is added, remove the previous file 1582 if (this.files.length > 1) { 1583 this.removeFile(this.files[0]); 1584 } 1585 }); 1586 1587 uploadProfile.on('thumbnail', function (file) { 1588 var minWidth = 150, 1589 minHeight = 150; 1590 1591 var element = this.element; 1592 1593 // if the added image does not meet the required dimensions remove it. 1594 if (file.width < minWidth || file.height < minHeight) { 1595 this.removeFile(this.files[0]); 1596 $(element).siblings('.error-input').find('span').html('Afbeeldingen dienen minimaal ' + minWidth + ' x ' + minHeight + ' px groot te zijn en kan alleen geüpload worden in de volgende formaten: .jpg, .png, .gif').end().fadeIn(); 1597 } else { 1598 $(element).siblings('.error-input').hide(); 1599 } 1600 }); 1601 1602 uploadProfile.on('sending', function (file, output) { 1603 $('#profile-picture').val(''); 1604 $('.upload-image-container .progress-bar-container').show(); 1605 }); 1606 1607 uploadProfile.on('uploadprogress', function (file, output) { 1608 1609 $('.upload-image-container .progress-bar-container .fill').width(output + '%'); 1610 $('.upload-image-container .progress-bar-container .number').text(parseInt(output) + '%'); 1611 }); 1612 1613 uploadProfile.on('complete', function (file, output) { 1614 1615 $('.upload-image-container .progress-bar-container').hide(); 1616 $('.upload-image-container .progress-bar-container .fill').width(0); 1617 $('.upload-image-container .progress-bar-container .number').text('0%'); 1618 }); 1619 1620 uploadProfile.on('success', function (file, output) { 1621 if (output.success) { 1622 $('#profile-picture').val(output.imagePath); 1623 } 1624 }); 1625 1626 $('form').on('submit', function (e) { 1627 uploadProfile.processQueue(); 1628 }); 1629 } 1630 1631 /************************************************ 1632 .bgl-ticket 1633 ************************************************/ 1634 $('.bgl-ticket').on('change selectmenuchange selectmenuselect', function () { 1635 bglTicket(); 1636 }); 1637 1638 function bglTicket() { 1639 if ($('.bgl-ticket').length === 0) { 1640 return; 1641 } 1642 1643 var number = parseInt($('.bgl-ticket').val()), 1644 container = $('#bgl-section'), 1645 inputs = $('.bgl-container input'); 1646 1647 number ? container.fadeIn() : container.fadeOut(); 1648 1649 inputs.slice(number, inputs.length).hide(); 1650 inputs.slice(0, number).show(); 1651 } 1652 1653 bglTicket(); 1654 1655 $('.bgl-container input').on( 1656 { 1657 keydown: function (e) { 1658 if (e.which === 32) { 1659 return false; 1660 } 1661 }, 1662 change: function () { 1663 this.value = this.value.replace(/\s/g, ""); 1664 } 1665 }); 1666 1667 /************************************************ 1668 OK Wallet 1669 ************************************************/ 1670 var okWalletSend = false; 1671 1672 function okWallet() { 1673 var uuid = $('#ok-wallet-qr').data('uuid'); 1674 var errorContainer = $('#ok-wallet-error-container'); 1675 var loadingScreen = $('#ok-wallet-screen'); 1676 var okWalletContainer = $('#ok-wallet-section'); 1677 1678 $.get('/api/wallet/ok/auth/status/' + uuid) 1679 .done(function (output) { 1680 var result = output.result; 1681 1682 if (result == 'OPEN') { 1683 if (okWalletSend === false) { 1684 setTimeout(okWallet, 1500); 1685 } 1686 1687 return; 1688 } 1689 1690 if (result == 'OK') { 1691 loadingScreen.fadeIn().css('display', 'table'); 1692 1693 $.get('/api/wallet/ok/auth/update/' + uuid) 1694 .done(function (output) { 1695 if (output.success && okWalletSend === false) { 1696 okWalletSend = true; 1697
1698 $.get('/api/wallet/ok/send/' + uuid) 1699 .done(function (output) { 1700 if (output.success) { 1701 $('#ok-wallet-loading').hide(); 1702 $('#ok-wallet-success').show(); 1703 loadingScreen.delay('2000').fadeOut(); 1704 1705 okWalletContainer.find('.col.tickets').hide(); 1706 okWalletContainer.find('.col.text .mobile').hide(); 1707 okWalletContainer.find('.col.text .desktop').hide(); 1708 okWalletContainer.find('.col.text .success').show(); 1709 return; 1710 } 1711 1712 loadingScreen.delay('2000').fadeOut(); 1713 okWalletContainer.find('.message-container').hide(); 1714 errorContainer.find('p').html(output.message); 1715 errorContainer.show(); 1716 return; 1717 }) 1718 .fail(function (output) { 1719 loadingScreen.delay('2000').fadeOut(); 1720 okWalletContainer.find('.message-container').hide(); 1721 errorContainer.find('p').html(output.responseJSON.message); 1722 errorContainer.show(); 1723 }); 1724 1725 return; 1726 } 1727 1728 loadingScreen.delay('2000').fadeOut(); 1729 okWalletContainer.find('.message-container').hide(); 1730 errorContainer.find('p').html(output.message); 1731 errorContainer.show(); 1732 return; 1733 }); 1734 1735 return; 1736 } 1737 1738 loadingScreen.delay('2000').fadeOut(); 1739 okWalletContainer.find('.message-container').hide(); 1740 errorContainer.find('p').html(output.message); 1741 errorContainer.show(); 1742 return; 1743 }); 1744 } 1745 1746 if ($('#ok-wallet-section').length && $('#ok-wallet-qr').data('uuid')) { 1747 okWallet(); 1748 } 1749 1750 /************************************************ 1751 tooltip image 1752 ************************************************/ 1753 $(document).tooltip( 1754 { 1755 items: '.tooltip-image', 1756 position: {my: "right center", at: "right top", collision: "flipfit"}, 1757 content: function () { 1758 var src = $(this).data('tooltip'), 1759 alt = $(this).attr('alt'); 1760 1761 var img = '<img src="#" alt="#" />' 1762 1763 return $(img).attr( 1764 { 1765 src: src, 1766 alt: alt 1767 }); 1768 } 1769 }); 1770 1771 /************************************************ 1772 #voucher-section 1773 ************************************************/ 1774 // display the number of voucher inputs 1775 function numberOfVouchers() { 1776 var number = ($('#number-of-vouchers').val() ? $('#number-of-vouchers').val() : 1); 1777 1778 $('#voucher-section .input-container').removeClass('active'); 1779 $('#voucher-section .input-container:lt(' + number + ')').addClass('active'); 1780 } 1781 1782 numberOfVouchers(); 1783 1784 $('#number-of-vouchers').on('change selectmenuchange', function () { 1785 numberOfVouchers(); 1786 }); 1787 1788 // checks if the voucher inputs have any value filled in 1789 function emptyVoucherCheck() { 1790 if ($('#voucher-section').length) { 1791 var empty = $('#voucher-section input').filter(function () { 1792 var val = $(this).val(); 1793 var value = val.trim().length; 1794 1795 return value > 0; 1796 }).length === 0; 1797 1798 empty ? $('#button-steps .next').fadeOut().prop('disabled', true) : $('#button-steps .next').fadeIn().prop('disabled', false); 1799 } 1800 } 1801 1802 emptyVoucherCheck(); 1803 1804 $('#voucher-section input').on('input keyup change', function () { 1805 emptyVoucherCheck(); 1806 }); 1807 1808 /************************************************ 1809 .toggle-container 1810 ************************************************/ 1811 $('.toggle-container .toggle-handle').on('click', function () { 1812 $(this).closest('.item').toggleClass('active'); 1813 }); 1814 1815 /************************************************ 1816 #refund-form 1817 ************************************************/ 1818 $('#refund-form form').on('submit', function (e) { 1819 e.preventDefault(); 1820 1821 $('#refund-request-confirmation-dialog').dialog('open'); 1822 }); 1823 1824 /************************************************ 1825 submit selfservice date 1826 ************************************************/ 1827 $('#submit-refund-request').not('.submit').on('click', function (e) { 1828 e.preventDefault(); 1829 var button = $(this); 1830 button.addClass('submit'); 1831 $('#refund-request-confirmation-dialog').addClass('disable-close-dialog'); 1832 1833 var name = $('#refund-name').val().trim(), 1834 email = $('#refund-email').val().trim(), 1835 ticket = $('#refund-ticket').val().trim(), 1836 comments = $('#refund-comments').val().trim(); 1837 1838 $.ajax({ 1839 url: '/script/refund-request', 1840 data: { 1841 name: name, 1842 email: email, 1843 ticket: ticket, 1844 comments: comments, 1845 language: language 1846 }, 1847 cache: false, 1848 dataType: 'json', 1849 method: 'POST', 1850 }).done(function (output) { 1851 $('#refund-request-confirmation-dialog').dialog('close'); 1852 1853 if (output.success) { 1854 $('#refund-form').hide(); 1855 $('#refund-send-success-container').show(); 1856 $('#refund-send-error-container').hide(); 1857 } else { 1858 $('#refund-form').show(); 1859 $('#refund-send-success-container').hide(); 1860 $('#refund-send-error-container').show(); 1861 } 1862 1863 button.removeClass('submit'); 1864 }); 1865 }); 1866 1867 /************************************************ 1868 disable submit after submitting form 1869 ************************************************/ 1870 var hasPerformedCheckout = false; // push checkout data to GTM before submitting form 1871 var hasPerformedSubmit = false; // submit checkout form only once 1872 1873 $('form').on('submit', function (e) { 1874 var paymentMethod = $('#payment-section input:checked').val(); 1875 1876 if (hasPerformedCheckout === false) { 1877 e.preventDefault(); 1878 1879 hasPerformedCheckout = typeof onCheckout === 'function' ? onCheckout(paymentMethod) : true; 1880 setTimeout(function () { 1881 $('.page-body form').submit() 1882 }, 500); 1883 } else if (hasPerformedSubmit === false) { 1884 hasPerformedSubmit = true; 1885 } else { 1886 e.preventDefault(); 1887 } 1888 }); 1889 1890 /************************************************ 1891 creditcard inputs 1892 ************************************************/ 1893 $('.creditcard-number').on('keydown', function (e) { 1894 var key = e.charCode || e.keyCode || 0; 1895
1896 // allow backspace, space, numbers and keypad numbers ONLY 1897 // tabs, home, end, period, and numpad decimal 1898 // arrow keys 1899 return ( 1900 key == 8 || 1901 key == 9 || 1902 key == 32 || 1903 (key >= 37 && key <= 40) || 1904 key == 110 || 1905 key == 190 || 1906 (key >= 48 && key <= 57) || 1907 (key >= 96 && key <= 105) 1908 ); 1909 }); 1910 1911 $('.creditcard-number').on('keyup', function (e) { 1912 var charLength = $(this).val().length; 1913 1914 var val = $(this).val().replace(/[^0-9]/g, ''), 1915 charLength = $(this).val().length, 1916 caretPos = e.target.selectionStart; 1917 1918 if (charLength === caretPos && charLength >= 4) { 1919 var newVal = val.match(/.{1,4}/g).join(' '); 1920 1921 $(this).val(newVal); 1922 } 1923 }); 1924 1925 $('.creditcard-name').on('keydown', function (e) { 1926 var key = e.keyCode; 1927 1928 if (key >= 48 && key <= 57) { 1929 return false; 1930 } 1931 }); 1932 1933 $('.creditcard-number, .creditcard-name').on('paste', function (e) { 1934 e.preventDefault(); 1935 }); 1936 1937 /************************************************ 1938 international phone numbers 1939 ************************************************/ 1940 var prefCountries = ["gb", "nl", "de", "fr", "pl", "es", "it", "pt", "ru", "cn", "jp", "ko"]; 1941 1942 if (language == 'nl') { 1943 prefCountries = ["nl", "gb", "de", "fr", "pl", "es", "it", "pt", "ru", "cn", "jp", "ko"]; 1944 } else if (language == 'de') { 1945 prefCountries = ["de", "nl", "gb", "fr", "pl", "es", "it", "pt", "ru", "cn", "jp", "ko"]; 1946 } else if (language == 'fr') { 1947 prefCountries = ["fr", "nl", "gb", "de", "pl", "es", "it", "pt", "ru", "cn", "jp", "ko"]; 1948 } else if (['es', 'ca'].indexOf(language) > -1) { 1949 prefCountries = ["es", "nl", "gb", "de", "fr", "pl", "it", "pt", "ru", "cn", "jp", "ko"]; 1950 } else if (language == 'it') { 1951 prefCountries = ["it", "nl", "gb", "de", "fr", "pl", "es", "pt", "ru", "cn", "jp", "ko"]; 1952 } else if (language == 'pt') { 1953 prefCountries = ["pt", "nl", "gb", "de", "fr", "pl", "es", "it", "ru", "cn", "jp", "ko"]; 1954 } else if (language == 'zh') { 1955 prefCountries = ["cn", "nl", "gb", "de", "fr", "pl", "es", "it", "pt", "ru", "jp", "ko"]; 1956 } else if (language == 'jp') { 1957 prefCountries = ["jp", "nl", "gb", "de", "fr", "pl", "es", "it", "pt", "ru", "cn", "ko"]; 1958 } else if (language == 'ru') { 1959 prefCountries = ["ru", "nl", "gb", "de", "fr", "pl", "es", "it", "pt", "cn", "jp", "ko"]; 1960 } 1961 1962 var $phoneNumber = $('#phone-number'); 1963 1964 if ($phoneNumber.length > 0) { 1965 if (defaultPhoneCountry) { 1966 prefCountries.unshift(defaultPhoneCountry); 1967 } 1968 1969 if ((smsVerificationEnabled === 'true' || smsVerificationEnabled === true) && typeof phoneOnlyCountries != 'undefined') { 1970 $phoneNumber.intlTelInput({ 1971 autoPlaceholder: "polite", 1972 formatOnDisplay: true, 1973 hiddenInput: "phone-number", 1974 onlyCountries: phoneOnlyCountries, 1975 }); 1976 } 1977 else if (smsVerificationEnabled === 'true' || smsVerificationEnabled === true) { 1978 $phoneNumber.intlTelInput({ 1979 autoPlaceholder: "polite", 1980 formatOnDisplay: true, 1981 hiddenInput: "phone-number", 1982 nationalMode: true, 1983 separateDialCode: true, 1984 preferredCountries: prefCountries, 1985 utilsScript: "/static/external/intlTelInput/utils.js" 1986 }); 1987 } 1988 else { 1989 $phoneNumber.intlTelInput({ 1990 autoPlaceholder: "polite", 1991 formatOnDisplay: true, 1992 hiddenInput: "phone-number", 1993 preferredCountries: prefCountries, 1994 }); 1995 } 1996 } 1997 1998 if (smsVerificationEnabled === 'true' || smsVerificationEnabled === true) { 1999 var $verifyCodeButton = $('#verify-code-button'); 2000 var $verificationCode = $('#verification_code'); 2001 var $smsVerificationContainer = $('#sms-verification-container'); 2002 var $verificationCodeError = $('#verification-code-error'); 2003 var $inputVerificationCodeContainer = $('#input-verification-code-container'); 2004 2005 $('.cm-sms-verification #telephone-info').on('click', function(e) { 2006 e.preventDefault(); 2007 if ($('#telephone-info-description').is(':hidden')) { 2008 $('#telephone-info-description').show(); 2009 } 2010 else { 2011 $('#telephone-info-description').hide(); 2012 } 2013 }); 2014 2015 $('.cm-sms-verification #sms-verification-container-show').on('click', function(e) { 2016 e.preventDefault(); 2017 $('#phone-number-contact-container').hide(); 2018 $('#sms-verification-container').show(); 2019 $('.adyen-web-component-wrapper').removeClass('hidden').show(); 2020 }); 2021 2022 $('.cm-sms-verification #phone-number-verify').not('.submit').on('click', function(e) { 2023 e.preventDefault(); 2024 var button = $(this); 2025 button.addClass('submit'); 2026 2027 if ($('#phone-number').val() == '') { 2028 $('#unverified-phone-number-error').show(); 2029 return; 2030 } 2031 2032 // Make ajax call to send sms 2033 $.ajax({ 2034 url: '/script/generate-sms', 2035 cache: false, 2036 dataType: 'json', 2037 data: {phoneNumber: $phoneNumber.intlTelInput('getNumber')}, 2038 method: 'POST', 2039 success: function (output) { 2040 /* clear loading */ 2041 loaded = true; 2042 2043 if (output.success === true) { 2044 $('#phone-number-contact-container').hide(); 2045 $('#sms-verification-container').show(); 2046 $('.adyen-web-component-wrapper').removeClass('hidden').show(); 2047 } 2048 else { 2049 $('#unverified-phone-number-error').show(); 2050 } 2051 } 2052 }); 2053 }); 2054 2055 $('input[name="payment"]').on('change', function(){ 2056 $('#sms-verification-container').hide(); 2057 if ($.inArray($(this).val(), ['visa', 'mastercard', 'american-express', 'vpay', 'bcmc', 'mc']) !== -1) { 2058 $('#phone-number-contact-container').show(); 2059 $('.adyen-web-component-wrapper').addClass('hidden').hide(); 2060 } 2061 else { 2062 $('.adyen-web-component-wrapper').removeClass('hidden').show(); 2063 $('#phone-number-contact-container').hide(); 2064 } 2065 }); 2066 2067 $verifyCodeButton.on('click', function (e) { 2068 e.preventDefault(); 2069 2070 if (!$verificationCode.val()) { 2071 $verificationCodeError.find('.message').text('Vul uw verificatiecode in'); 2072 $verificationCodeError.show(); 2073 return; 2074 } 2075 2076 $smsVerificationContainer.addClass('loading'); 2077 2078 $('#verification_code').val($verificationCode.val()); 2079 $('#summary-form').submit(); 2080 }); 2081 2082 $inputVerificationCodeContainer.on('input', function () { 2083 var verificationCode = ''; 2084 var codes = $(this).find('.code'); 2085 2086 codes.each(function () { 2087 verificationCode += $(this).val(); 2088 }); 2089 2090 $verificationCode.val(verificationCode); 2091 }); 2092 2093 $inputVerificationCodeContainer.find('.code').on('keyup', function (e) { 2094 var keycode = e.keyCode; 2095 var code = $(this); 2096 2097 $verificationCodeError.hide(); 2098 2099 if (keycode === 8) { 2100 code.prev('.code').focus().select(); 2101 return; 2102 } 2103 2104 if (code.val().length === 1) { 2105 code.next('.code').focus().select(); 2106 } 2107 }); 2108 } 2109 2110 2111 /************************************************ 2112 create QR code 2113 ************************************************/ 2114 if ($('#qrcode-vaccine').length) { 2115 var qrcode = new QRCode(document.getElementById("qrcode-vaccine"), { 2116 width: 160, 2117 height: 160 2118 }); 2119 2120 function makeCode() { 2121 qrcode.makeCode('thisIsAVaccineCode'); 2122 } 2123 2124 makeCode(); 2125 2126 $('#print-page').on('click', function (e) { 2127 e.preventDefault(); 2128 2129 window.print(); 2130 }); 2131 } 2132 2133 /************************************************ 2134 Chosen date and time display 2135 ************************************************/ 2136 function chosenDateAndTimeText() { 2137 $('#chosen-date-time-section').hide(); 2138
2139 if ($('#calendar-date').val() && $('#timeslot-section .timeslot input:checked').length) { 2140 var date = new Date($('#calendar-date').val()), 2141 dateText = $.datepicker.formatDate('DD d MM yy', date), 2142 time = $('#timeslot-section .timeslot input:checked').val(); 2143 2144 $('#chosen-date-text').text(dateText); 2145 $('#chosen-time-text').text(time); 2146 $('#chosen-date-time-section').show(); 2147 } 2148 } 2149 2150 $('#calendar-date').on('change', function () { 2151 chosenDateAndTimeText(); 2152 }); 2153 2154 $('#timeslot-section .daypart-container > input').on('change', function () { 2155 $('#timeslot-section .timeslot input:checked').prop('checked', false); 2156 }); 2157 2158 $('#timeslot-section').on('change', '.timeslot input, .daypart-container', function () { 2159 chosenDateAndTimeText(); 2160 }); 2161 2162 /************************************************ 2163 sms verification 2164 ************************************************/ 2165 if (typeof phonenumber !== 'undefined' && typeof email !== 'undefined') { 2166 var sendSms = false; 2167 $('#summary-form').submit(function (e) { 2168 if ($('#verification_reference').val().length && $('#verification_code').val().length) { 2169 return; 2170 } else if (sendSms === true) { 2171 e.preventDefault(); 2172 return; 2173 } 2174 e.preventDefault(); 2175 2176 sendSms = true; 2177 postData('https://api-cm.globalticket.com/v1/public/sms/generate?phone_number=' + phonenumber) 2178 .then(function (data) { 2179 $('#verification_reference').val(data.verification_reference); 2180 }); 2181 2182 $('#vaccination-reservation-data-overview, #button-steps').hide(); 2183 $('#vaccination-verification-overview, #sms-verification-container').show(); 2184 }); 2185 } 2186 2187 // Dynamic-pricing calendar labels live in static/js/calendar-pricing.js 2188}); 2189 2190// display next button or get timeslots 2191function ticketDateChosen(ticketDate, formattedDate) { 2192 $('#calendar-date').change(); 2193 dateCondition(); 2194 2195 if (settingsTimeslots) { 2196 getTimeslots(ticketDate, formattedDate); 2197 } else { 2198 if (hasTicketsSelected()) { 2199 showNextButton(); 2200 } 2201 } 2202 2203 if(typeof window.customCalendarDateSelectEvent != 'undefined') { 2204 window.customCalendarDateSelectEvent(ticketDate, formattedDate); 2205 } 2206} 2207 2208function hasTicketsSelected() { 2209 return ticketsTotalNumber > 0; 2210} 2211 2212function hideNextButton() { 2213 $('#button-steps .next').hide().prop('disabled', true); 2214} 2215 2216function showNextButton() { 2217 $('#button-steps .next').show().prop('disabled', false); 2218} 2219 2220// Get Timeslots Ajax call 2221function getTimeslots(ticketDate, formattedDate) { 2222 if (typeof disabledDates != 'undefined' && $.inArray(ticketDate, disabledDates) >= 0) { 2223 // Do not get timeslots if the date is disabled 2224 return; 2225 } 2226 2227 // Do not get timeslots if gift ticket is checked 2228 if ($('#gift-ticket').prop('checked')) { 2229 return; 2230 } 2231 2232 /* initialize timeslots */ 2233 var $timeslotSection = $('#timeslot-section'); 2234 var $timeslotsLoading = $timeslotSection.find('.timeslots-loading'); 2235 var $timeslotsContainer = $('.timeslots-container'); 2236 var $daypartContainer = $('.daypart-container'); 2237 var $chosenDateTimeSection = $('#chosen-date-time-section'); 2238 2239 var $timeslotsSoldout = $('#timeslots-soldout'); 2240 var $timeslotsOverlimit = $('#timeslots-overlimit'); 2241 var $timeslotsTexts = $('.timeslots-container-top, .timeslots-container-bottom'); 2242 var $timeslotErrorContainer = $timeslotSection.find('.error-container'); 2243 var $buttonStepsNext = $('#button-steps .next'); 2244 2245 /* ajax call */ 2246 if (currentRequest != null) { 2247 currentRequest.abort(); 2248 } 2249 currentRequest = $.ajax({ 2250 url: '/script/timeslots', 2251 cache: false, 2252 dataType: 'json', 2253 data: { 2254 tag: $('html').attr('data-tag'), 2255 eventId: (eventId ? eventId : null), 2256 productEventId: (productEventId ? productEventId : ''), 2257 ticketDate: ticketDate, 2258 ticketNumber: ticketsTotalNumber, 2259 ticketNumbers: ticketNumbers, 2260 timeslotsGroup: timeslotsGroup, 2261 streetname: streetname 2262 }, 2263 method: 'POST', 2264 beforeSend: function () { 2265 $timeslotsLoading.show(); 2266 $timeslotsContainer.empty(); 2267 $daypartContainer.hide().find('.timeslot').remove(); 2268 $chosenDateTimeSection.hide(); 2269 $timeslotsSoldout.hide(); 2270 $timeslotsOverlimit.hide(); 2271 $timeslotsTexts.hide(); 2272 $buttonStepsNext.hide().prop('disabled', true); 2273 $timeslotSection.slideDown(); 2274 $timeslotsContainer.removeClass('has-expirations has-soldouts'); 2275 } 2276 }).done(function (output) { 2277 // Don't render timeslots if gift ticket is now checked 2278 if ($('#gift-ticket').prop('checked')) { 2279 return; 2280 } 2281 2282 $timeslotsLoading.hide(); 2283 2284 if (output.success) { 2285 var timeslotsSoldout = true; 2286 var hasRemaining = 0; 2287 2288 /* Flatpickr */ 2289 if (timeslotsByDayParting) { 2290 var morningTimeslots = []; 2291 var afternoonTimeslots = []; 2292 var eveningTimeslots = []; 2293 2294 $.each(output.timeslots, function (index, timeslot) { 2295 if (settingsHideExpiredTimeslots === true && timeslot.expired) { 2296 return; 2297 } 2298 2299 if (timeslot.active || settingsDisplayInactiveTimeslots) { 2300 var timeslotElement = $('<div class="timeslot"></div>'); 2301 var timeslotInput = $('<input type="radio" name="ticketTime">'); 2302 var timeslotLabel = $('<label></label>'); 2303 var id = timeslot.time.replace(':', ''); 2304 var timeslotTitle = settingsDisableTimeslotsTitle ? timeslot.time : timeslot.title; 2305 2306 timeslotElement.data('timeslot', timeslot.time).addClass(timeslot.classAttr); 2307 timeslotLabel.attr('title', timeslotTitle + ' uur').text(timeslotTitle); 2308 2309 if (settingsDisplayInactiveTimeslots === true && timeslot.active === false) { 2310 timeslotLabel.addClass('inactive'); 2311 2312 if (timeslot.expired === true) { 2313 $timeslotsContainer.addClass('has-expirations'); 2314 } 2315 if (timeslot.soldOut === true) { 2316 timeslotLabel.addClass('soldout'); 2317 $timeslotsContainer.addClass('has-soldouts'); 2318 } 2319 } else { 2320 var chosenTicketDateTime = chosenTicketDate === ticketDate && chosenTicketTime === timeslot.time; 2321 2322 timeslotInput.attr({ 2323 id: 'timeslot-' + id, 2324 title: timeslotTitle, 2325 value: timeslot.time, 2326 'aria-label': formattedDate ? (formattedDate + ', ' + timeslot.time) : timeslot.time, 2327 }).prop('checked', chosenTicketDateTime); 2328 2329 timeslotLabel.attr({ 2330 for: 'timeslot-' + id, 2331 title: timeslot.time + ' uur' 2332 }); 2333 2334 timeslotElement.append(timeslotInput); 2335 } 2336 2337 if (output.showAvailableTickets && output.showAvailableTickets === true && timeslot.availableTickets !== null) { 2338 timeslotLabel.append('<span class="available-tickets">' + timeslot.availableTickets + ' ' + LANG_AVAILABLE + '</span>'); 2339 } 2340 2341 timeslotElement.append(timeslotLabel); 2342 2343 if (parseInt(id) >= 0 && parseInt(id) < 1200) { 2344 morningTimeslots.push(timeslotElement); 2345 } else if (parseInt(id) >= 1200 && parseInt(id) < 1800) { 2346 afternoonTimeslots.push(timeslotElement); 2347 } else { 2348 eveningTimeslots.push(timeslotElement); 2349 } 2350 2351 timeslotsSoldout = false;
2352 } else { 2353 if (timeslot.expired === false) { 2354 hasRemaining += timeslot.totalAvailable; 2355 } 2356 } 2357 }); 2358 2359 if (morningTimeslots.length) { 2360 $('#morning').prop('disabled', false); 2361 $('#morning-timeslots').html(morningTimeslots); 2362 } 2363 if (afternoonTimeslots.length) { 2364 $('#afternoon').prop('disabled', false); 2365 $('#afternoon-timeslots').html(afternoonTimeslots); 2366 } 2367 if (eveningTimeslots.length) { 2368 $('#evening').prop('disabled', false); 2369 $('#evening-timeslots').html(eveningTimeslots); 2370 } 2371 2372 /* show available tickets */ 2373 if (output.showAvailableTickets && output.showAvailableTickets === true) { 2374 $daypartContainer.addClass('show-available-tickets'); 2375 } 2376 } else { /* Jquery UI Datepicker */ 2377 var html = ''; 2378 2379 $.each(output.timeslots, function (index, timeslot) { 2380 if (settingsHideExpiredTimeslots === true && timeslot.expired) { 2381 return; 2382 } 2383 var timeslotTitle = settingsDisableTimeslotsTitle ? timeslot.time : timeslot.title; 2384 2385 if (timeslot.active || settingsDisplayInactiveTimeslots) { 2386 /* id */ 2387 var id = timeslot.time.replace(':', ''); 2388 var classAttr = 'gt-background-second timeslot'; 2389 2390 if (timeslot.classAttr) { 2391 classAttr += ' ' + timeslot.classAttr; 2392 } 2393 2394 /* html */ 2395 html += '<li class="' + classAttr + '" data-timeslot="' + timeslot.time + '">'; 2396 2397 if (settingsDisplayInactiveTimeslots === true && timeslot.active === false && timeslot.expired === true) { 2398 html += '<label class="inactive">' + timeslotTitle; 2399 2400 $timeslotsContainer.addClass('has-expirations'); 2401 } else if (settingsDisplayInactiveTimeslots === true && timeslot.active === false && timeslot.soldOut === true) { 2402 html += '<label class="inactive soldout">' + timeslotTitle; 2403 2404 $timeslotsContainer.addClass('has-soldouts'); 2405 } else if (timeslot.classAttr === 'timeslotAlmostFull') { 2406 var chosenTicketDateTime = chosenTicketDate === ticketDate && chosenTicketTime === timeslot.time; 2407 html += '<input type="radio" id="timeslot-' + id + '" name="ticketTime" value="' + timeslot.time + '"' + (chosenTicketDateTime ? ' checked' : '') + '>'; 2408 html += '<label for="timeslot-' + id + '" class="almost-gone gt-background-main gt-border-color-third">' + timeslotTitle; 2409 2410 $timeslotsContainer.addClass('almost-full'); 2411 } else { 2412 var chosenTicketDateTime = chosenTicketDate === ticketDate && chosenTicketTime === timeslot.time; 2413 2414 html += '<input type="radio" id="timeslot-' + id + '" name="ticketTime" value="' + timeslot.time + '"' + (chosenTicketDateTime ? ' checked' : '') + '>'; 2415 html += '<label for="timeslot-' + id + '" class="gt-background-main gt-border-color-third">' + timeslotTitle; 2416 } 2417 2418 if (output.showAvailableTickets && output.showAvailableTickets === true && timeslot.availableTickets !== null) { 2419 html += '<span class="available-tickets">' + timeslot.availableTickets + ' ' + LANG_AVAILABLE + '</span>'; 2420 } 2421 2422 html += '</label>'; 2423 html += '</li>'; 2424 2425 timeslotsSoldout = false;
2426 } else { 2427 if (timeslot.expired === false) { 2428 hasRemaining += timeslot.totalAvailable; 2429 } 2430 } 2431 }); 2432 2433 /* show available tickets */ 2434 if (output.showAvailableTickets && output.showAvailableTickets === true) { 2435 $timeslotsContainer.addClass('show-available-tickets'); 2436 } 2437 2438 $timeslotsContainer.html(html); 2439 2440 // Ensure the first timeslot is focused when a date is chosen, streamlining the process for users navigating solely with a keyboard to select a slot more efficiently. 2441 const $timeslots = $timeslotsContainer.find('input[name="ticketTime"]'); 2442 if($timeslots.length > 0 && firstPageLoad === false) { 2443 $timeslots.first().focus(); 2444 } 2445 } 2446 2447 document.dispatchEvent(new CustomEvent('timeslots:rendered', { detail: { date: ticketDate } })); 2448 2449 /* show custom timeslot styling */ 2450 if (settingsCustomTimeslotStyling === true) { 2451 $timeslotsContainer.find('li label').removeClass('gt-background-main gt-border-color-third'); 2452 $timeslotsContainer.find('li').removeClass('gt-background-second'); 2453 } 2454 2455 if (hasRemaining > 0 && timeslotsSoldout) { 2456 $timeslotsSoldout.hide(); 2457 $timeslotsOverlimit.show(); 2458 $timeslotsTexts.hide(); 2459 $timeslotSection.removeClass('completed'); 2460 $buttonStepsNext.hide().prop('disabled', true); 2461 } else if (timeslotsSoldout) { 2462 $timeslotsSoldout.show(); 2463 $timeslotsOverlimit.hide(); 2464 $timeslotsTexts.hide(); 2465 $timeslotSection.removeClass('completed'); 2466 $buttonStepsNext.hide().prop('disabled', true); 2467 } else { 2468 $daypartContainer.slideDown(); 2469 $timeslotsSoldout.hide(); 2470 $timeslotsOverlimit.hide() 2471 $timeslotsTexts.show(); 2472 } 2473 2474 let autoselectFirstTimeslotIfOne = true; 2475 if (typeof(window.settingAutoselectFirstTimeslotIfOne) != 'undefined') { 2476 autoselectFirstTimeslotIfOne = window.settingAutoselectFirstTimeslotIfOne; 2477 } 2478 if (autoselectFirstTimeslotIfOne) { 2479 if ($timeslotsContainer.find('.timeslot input').length === 1) { 2480 $timeslotsContainer.find('.timeslot input').prop('checked', true).change(); 2481 } else if ($timeslotsContainer.find('input:checked').length === 1) { 2482 $timeslotsContainer.find('.timeslot input').change(); 2483 } 2484 } 2485 2486 $timeslotSection.slideDown().change(); 2487 } 2488 2489 if (typeof homeUrl == 'undefined' && (scrollToTimeslotsOnPageLoad === true || firstPageLoad === false)) { 2490 window.scrollTo({ 2491 top: $timeslotSection.offset().top - 50, 2492 behavior: 'smooth' 2493 }); 2494 } 2495 2496 firstPageLoad = false; 2497 timeslotCondition(); 2498 displayNextButton(); 2499 }); 2500 2501 if(typeof window.customTimeslotLoadedEvent != 'undefined') { 2502 window.customTimeslotLoadedEvent(); 2503 } 2504 2505 dateCondition(); 2506 hideNextButton(); 2507} 2508 2509function getMainSelectedTimeslot() { 2510 return $('#timeslot-section .timeslots-container input:checked').prop('checked', true).val(); 2511} 2512 2513/************************************************ 2514 Date page: next button 2515 ************************************************/ 2516// return true or false if date is selected 2517// and if timeslots exist, return true or false if date and timeslot are selected 2518function dateCondition() { 2519 var section = $('#calendar-section'); 2520 var condition = section.find('#calendar-date').val() != ''; 2521 2522 sectionCompleted(section, condition); 2523} 2524 2525function timeslotCondition() { 2526 var section = $('#timeslot-section'); 2527 var condition = section.find('input').is(':checked'); 2528 2529 condition ? $('#timeslot-availability-section').slideDown() : $('#timeslot-availability-section').slide
2529Up(); 2530 2531 sectionCompleted(section, condition); 2532} 2533 2534// display next button if all required section has class "completed" 2535function displayNextButton() { 2536 var nextButton = $('#button-steps .button.next'); 2537 var completed = $('.section-container .section.required:not(.completed)').length === 0; 2538 2539 completed ? showNextButton() : hideNextButton(); 2540} 2541 2542/************************************************ 2543 Section is completed 2544 ************************************************/ 2545// if section is completed, add class 'completed' 2546function sectionCompleted(element, condition) { 2547 if (condition) { 2548 element.addClass('completed'); 2549 } else { 2550 element.removeClass('completed'); 2551 } 2552} 2553 2554function getUpsellTickets() 2555{ 2556 var data = [], 2557 upsell = $('#custom-upsell'), 2558 items = upsell.find('.item'); 2559 2560 $.each(items, function (i, item) { 2561 var item = $(this); 2562 var selectMenuItems = item.find('.quantity select'); 2563 2564 $.each(selectMenuItems, function (j, select) { 2565 let title = $(select).data('title'), 2566 price = $(select).data('price'), 2567 fromPrice = $(select).data('from-price'), 2568 quantity = $(select).val(), 2569 timeslot = item.find('.timeslot select').val(), 2570 ticketGroupId = $(select).data('ticket-group-id'); 2571 2572 //no item selected: don't push to cart 2573 if(parseInt(quantity) === 0) { 2574 return; 2575 } 2576 2577 fromPrice = parseFloat(fromPrice); 2578 price = parseFloat(price); 2579 difference = fromPrice < price ? 0 : (fromPrice - price); 2580 2581 //if the item has no timeslots: push to cart 2582 if (item.hasClass('no-timeslots')) { 2583 data.push({ 2584 description: title, 2585 fromPrice: fromPrice, 2586 price: price, 2587 number: parseInt(quantity), 2588 subtotal: parseInt(quantity) * parseFloat(price), 2589 difference: difference 2590 }); 2591 2592 return; 2593 } 2594 2595 // if item has timeslots but there is no timeslot selected: don't push to cart 2596 if(timeslot === 'false' || timeslot === false || timeslot === null) { 2597 return; 2598 } 2599 2600 //quantity and timeslot selected: push to cart 2601 data.push({ 2602 description: title, 2603 fromPrice: fromPrice, 2604 price: price, 2605 number: parseInt(quantity), 2606 subtotal: parseInt(quantity) * parseFloat(price), 2607 difference: difference 2608 }); 2609 }); 2610 }); 2611 2612 return data; 2613} 2614 2615var currentRequest = null; 2616 2617function postData(url, data) { 2618 currentRequest = $.ajax({ 2619 url: url, 2620 data: data, 2621 cache: false, 2622 dataType: 'json', 2623 method: 'POST', 2624 beforeSend: function () { 2625 if (currentRequest != null) { 2626 currentRequest.abort(); 2627 } 2628 }, 2629 success: function (output) { 2630 return output; 2631 } 2632 }); 2633 2634 return currentRequest; 2635} 2636 2637// Prevent resubmission of the form when user hit Control/Cmd + R or browser refresh button 2638if (window.history.replaceState) { 2639 window.history.replaceState(null, null, window.location.href); 2640} 2641 2642/************************************************ 2643Accessibility menu 2644************************************************/ 2645$('.accessibility-menu-toggle').on('click', () => { 2646 $('.accessibility-menu-container').toggleClass('active'); 2647}); 2648 2649$('.accessibility-menu .toggle-increase-contrast').on('click', (e) => { 2650 $('.page-body').toggleClass('increase-contrast'); 2651 $(e.currentTarget).toggleClass('active'); 2652 document.cookie = "_wcag_increase_contrast=" + $('.page-body').hasClass('increase-contrast') + "; path=/"; 2653}); 2654 2655$('.accessibility-menu .toggle-increase-font-size').on('click', (e) => { 2656 $('.page-body').toggleClass('increase-font-size'); 2657 $(e.currentTarget).toggleClass('active'); 2658 document.cookie = "_wcag_increase_fontsize=" + $('.page-body').hasClass('increase-font-size') + "; path=/"; 2659}); 2660 2661/************************************************ 2662 Availability page accessibility 2663 ************************************************/ 2664const stepEl = document.getElementById('step-name'); 2665if (stepEl && stepEl.dataset.stepName === 'date') { 2666 (function () { 2667 /* ----------------------------------------------------------- 2668 0. Lazy helpers to read serverâside translations once 2669 ----------------------------------------------------------- */ 2670 let _timeslotTranslations = null; 2671 const getTimeslotTranslations = () => { 2672 if (_timeslotTranslations) return _timeslotTranslations; 2673 const el = document.getElementById('timeslot-translations'); 2674 _timeslotTranslations = el 2675 ? JSON.parse(el.dataset.translations)
2676 : { timeslot: '', almostFull: '', busy: '', closed: '' }; 2677 return _timeslotTranslations; 2678 }; 2679 2680 let _calendarTranslations = null; 2681 const getCalendarTranslations = () => { 2682 if (_calendarTranslations) return _calendarTranslations; 2683 const el = document.getElementById('calendar-month-translations'); 2684 _calendarTranslations = el 2685 ? JSON.parse(el.dataset.calendarMonthTranslations) 2686 : { currentMonth: '', previousMonth: '', nextMonth: '' }; 2687 return _calendarTranslations; 2688 }; 2689 2690 /* ----------------------------------------------------------- 2691 1. Timeslot list (<li><label>) accessibility 2692 ----------------------------------------------------------- */ 2693 const makeLabelsFocusable = () => { 2694 const t = getTimeslotTranslations(); 2695 document.querySelectorAll('.timeslots-container li label').forEach(label => { 2696 label.tabIndex = 0; 2697 label.setAttribute('role', 'button'); 2698 2699 const timeText = (label.firstChild?.nodeType === Node.TEXT_NODE) 2700 ? label.firstChild.textContent.trim() 2701 : ''; 2702 2703 const availabilitySpan = label.querySelector('.available-tickets'); 2704 const availabilityText = availabilitySpan ? availabilitySpan.textContent.trim() : ''; 2705 2706 let labelText = t.timeslot + timeText; 2707 if (availabilityText) labelText += ', ' + availabilityText; 2708 2709 const li = label.closest('li'); 2710 if (li) { 2711 if (li.classList.contains('timeslotAlmostFull')) labelText += ', ' + t.almostFull; 2712 if (li.classList.contains('timeslotBusy')) labelText += ', ' + t.busy; 2713 if (li.classList.contains('timeslotClosed')) labelText += ', ' + t.closed; 2714 } 2715 2716 label.setAttribute('title', labelText); 2717 label.setAttribute('aria-label', labelText); 2718 }); 2719 }; 2720 2721 /* ----------------------------------------------------------- 2722 2. Calendarâday links accessibility 2723 ----------------------------------------------------------- */ 2724 const makeCalendarDaysAccessible = () => { 2725 const datepicker = document.querySelector('.ui-datepicker'); 2726 if (!datepicker) return; 2727 2728 const dayNames = Array.from( 2729 datepicker.querySelectorAll('.ui-datepicker-calendar thead th span') 2730 ).map(span => span.getAttribute('title') || ''); 2731 2732 datepicker.querySelectorAll('.ui-datepicker-calendar a').forEach(link => { 2733 const dayNumber = link.textContent.trim(); 2734 const cell = link.closest('td'); 2735 if (!cell) return; 2736 2737 const cellIndex = Array.from(cell.parentNode.children).indexOf(cell); 2738 const dayName = dayNames[cellIndex] || ''; 2739 2740 const labelText = `${dayName} ${dayNumber}`; 2741 link.setAttribute('title', labelText); 2742 link.setAttribute('aria-label', labelText); 2743 }); 2744 }; 2745 2746 /* ----------------------------------------------------------- 2747 3. Month header & nav: reorder nodes + aria 2748 ----------------------------------------------------------- */ 2749 const attachMonthNav = (datepicker = document.querySelector('.ui-datepicker')) => { 2750 if (!datepicker) return; 2751 2752 const header = datepicker.querySelector('.ui-datepicker-header'); 2753 if (!header) return; 2754 2755 const title = header.querySelector('.ui-datepicker-title'); 2756 const prevButton = header.querySelector('.ui-datepicker-prev'); 2757 const nextButton = header.querySelector('.ui-datepicker-next'); 2758 2759 /* 3a. Put the title first in the DOM so natural tabâorder is ok */ 2760 if (title && header.firstChild !== title) { 2761 header.insertBefore(title, header.firstChild); // title, prev, next 2762 } 2763 2764 /* 3b. Normal tabindex (0) so every browser sees them */
2765 [title, prevButton, nextButton].forEach(el => { 2766 if (!el) return; 2767 el.removeAttribute('tabindex'); 2768 el.tabIndex = 0; 2769 }); 2770 if (prevButton?.classList.contains('ui-state-disabled')) prevButton.removeAttribute('tabindex'); 2771 2772 /* 3c. Accessible names */ 2773 const t = getCalendarTranslations(); 2774 2775 if (title) { 2776 const month = title.querySelector('.ui-datepicker-month')?.textContent ?? ''; 2777 const year = title.querySelector('.ui-datepicker-year')?.textContent ?? ''; 2778 title.setAttribute('role', 'heading'); 2779 title.setAttribute('aria-level', '2'); 2780 title.setAttribute('aria-label', (t.currentMonth ?? '') + `${month} ${year}`.trim()); 2781 } 2782 if (prevButton && !prevButton.classList.contains('ui-state-disabled')) { 2783 prevButton.setAttribute('role', 'button'); 2784 prevButton.setAttribute('aria-label', t.previousMonth ?? 'Previous month'); 2785 } 2786 if (nextButton) { 2787 nextButton.setAttribute('role', 'button'); 2788 nextButton.setAttribute('aria-label', t.nextMonth ?? 'Next month'); 2789 } 2790 }; 2791 2792 /* ----------------------------------------------------------- 2793 4. Focus handling for âsoldâoutâ message (unchanged) 2794 ----------------------------------------------------------- */ 2795 const setupSoldOutFocus = () => { 2796 const soldOutMessage = document.getElementById('timeslots-soldout'); 2797 let lastActiveDate = null; 2798 2799 document.addEventListener('keydown', e => { 2800 if (e.key === 'Enter' || e.key === ' ') { 2801 const calendarDate = e.target.closest('.ui-datepicker-calendar a'); 2802 if (calendarDate) lastActiveDate = calendarDate; 2803 } 2804 }, true); 2805 2806 if (!soldOutMessage) return; 2807 2808 document.addEventListener('keydown', e => { 2809 if (e.key === 'Tab' && !e.shiftKey && 2810 soldOutMessage.style.display === 'block' && 2811 document.activeElement === soldOutMessage) { 2812 2813 e.preventDefault(); 2814 (lastActiveDate && document.body.contains(lastActiveDate) 2815 ? lastActiveDate 2816 : document.querySelector('.ui-datepicker-calendar a[href]')) 2817 ?.focus(); 2818 } 2819 }, true); 2820 2821 new MutationObserver(muts => { 2822 muts.forEach(mut => { 2823 if (mut.attributeName === 'style' && soldOutMessage.style.display === 'block') { 2824 setTimeout(() => soldOutMessage.focus(), 0); 2825 } 2826 }); 2827 }).observe(soldOutMessage, { attributes: true, attributeFilter: ['style'] }); 2828 }; 2829 2830 /* ----------------------------------------------------------- 2831 5. Generic errorâcontainer focus trap (unchanged) 2832 ----------------------------------------------------------- */ 2833 const trapErrorFocus = el => { 2834 let trap = true; 2835 const releaseTrap = () => { trap = false; }; 2836 2837 el.addEventListener('blur', () => { 2838 setTimeout(() => { 2839 if (document.activeElement !== el && trap) el.focus(); 2840 }, 10); 2841 }); 2842 el.addEventListener('keydown', e => { 2843 if (e.key === 'Tab') releaseTrap(); 2844 }); 2845 // Release trap on click/touch anywhere 2846 document.addEventListener('mousedown', releaseTrap, { once: true }); 2847 document.addEventListener('touchstart', releaseTrap, { once: true }); 2848 }; 2849 2850 const focusErrorContainer = () => { 2851 const error = document.querySelector('.error-container[role="alert"]'); 2852 if (!error) return; 2853 if (!error.hasAttribute('tabindex')) error.tabIndex = 0; 2854 setTimeout(() => { 2855 error.focus(); 2856 trapErrorFocus(error); 2857 error.scrollIntoView({ behavior: 'smooth', block: 'center' }); 2858 }, 0); 2859 }; 2860 2861 /* ----------------------------------------------------------- 2862 6. Oneâtime setup on initial load 2863 ----------------------------------------------------------- */ 2864 document.addEventListener('DOMContentLoaded', () => { 2865 makeLabelsFocusable(); 2866 attachMonthNav(); 2867 makeCalendarDaysAccessible(); 2868 setupSoldOutFocus(); 2869 focusErrorContainer(); 2870 }); 2871 2872 /* ----------------------------------------------------------- 2873 7. Observe calendar container for every redraw 2874 ----------------------------------------------------------- */ 2875 const containers = document.querySelectorAll( 2876 '.calendar-container, #timeslot-section' 2877 ); 2878 if (containers.length) { 2879 const observer = new MutationObserver(() => { 2880 makeLabelsFocusable(); // when timeslots appear/refresh 2881 attachMonthNav(); // when calendar header redraws 2882 makeCalendarDaysAccessible(); // when days change 2883 setupSoldOutFocus(); // keep soldâout focus behavior 2884 });
2885 containers.forEach(c => 2886 observer.observe(c, { childList: true, subtree: true }) 2887 ); 2888 } 2889 2890 /* ----------------------------------------------------------- 2891 8. Enter / Space triggers for pseudoâbuttons & links 2892 ----------------------------------------------------------- */ 2893 document.addEventListener('keydown', e => { 2894 if (e.key !== 'Enter' && e.key !== ' ') return; 2895 2896 const clickable = e.target.closest( 2897 '.ui-datepicker-calendar a, .timeslots-container li label, .ui-datepicker-prev, .ui-datepicker-next' 2898 ); 2899 if (!clickable) return; 2900 2901 if (clickable.matches('.ui-datepicker-prev, .ui-datepicker-next') && 2902 clickable.getAttribute('tabindex') === '0') { 2903 e.preventDefault(); 2904 clickable.click(); 2905 } else if (!clickable.matches('.ui-datepicker-prev.ui-state-disabled')) { 2906 e.preventDefault(); 2907 clickable.click(); 2908 } 2909 }); 2910 })(); 2911} 2912 2913/************************************************ 2914 Donation flow accessibility 2915 ************************************************/ 2916document.addEventListener('DOMContentLoaded', () => { 2917 // --- Part 1: Listeners for donation price options 2918 const optionsContainer = document.querySelector('#donation-buttons'); 2919 if (!optionsContainer) return; 2920 2921 const options = Array.from(optionsContainer.querySelectorAll('[role="radio"]')); 2922 2923 options.forEach((opt, i) => 2924 opt.setAttribute('tabindex', i === 0 ? '0' : '-1') 2925 ); 2926 2927 options.forEach((option, currentIndex) => { 2928 option.addEventListener('keydown', event => { 2929 let newIndex; 2930 2931 switch (event.key) { 2932 case 'ArrowRight': 2933 case 'ArrowDown': 2934 newIndex = (currentIndex + 1) % options.length; 2935 break; 2936 2937 case 'ArrowLeft': 2938 case 'ArrowUp': 2939 newIndex = (currentIndex - 1 + options.length) % options.length; 2940 break; 2941 2942 case 'Enter': 2943 case ' ': 2944 event.preventDefault(); 2945 const radio = option.querySelector('input[type="radio"]'); 2946 if (radio) radio.click(); 2947 return; 2948 2949 default: 2950 return; 2951 } 2952 2953 event.preventDefault(); 2954 moveFocus(options, currentIndex, newIndex); 2955 }); 2956 }); 2957 2958 function moveFocus(items, oldIndex, newIndex) { 2959 items[oldIndex].setAttribute('tabindex', '-1'); 2960 items[newIndex].setAttribute('tabindex', '0'); 2961 items[newIndex].focus(); 2962 } 2963 2964 // --- Part 2: Listeners for other elements of payment and thank you page 2965 const otherClickables = [ 2966 ...document.querySelectorAll('.js-donation-retry'), 2967 ...document.querySelectorAll('.js-payment-method'), 2968 ...document.querySelectorAll('.js-more-payments'), 2969 ...document.querySelectorAll('.js-faq') 2970 ]; 2971
2972 otherClickables.forEach(el => { 2973 el.addEventListener('keydown', event => { 2974 if (event.key === 'Enter' || event.key === ' ') { 2975 event.preventDefault(); 2976 if (el.classList.contains('js-payment-method')) { 2977 const radio = el.querySelector('input[type="radio"]'); 2978 if (radio) radio.click(); 2979 } else if (el.classList.contains('js-faq')) { 2980 const link = el.querySelector('a'); 2981 if (link) link.click(); 2982 } else { 2983 el.click(); 2984 } 2985 } 2986 }); 2987 }); 2988}); 2989 2990
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.