PageSourceSearch

https://tickets.maritiemmuseum.nl/static/js/branded.js?v1.0.1495

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

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.