PageSourceSearch

https://tickets.nxtmuseum.com/static/js/branded.js?v1.0.1494

js nxtmuseum.com collected 2026-09-24 19:18:11 UTC 115,076 bytes, 2,990 lines download raw bytes

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.