PageSourceSearch

https://aisummitbudapest.hu/assets/js/event/agenda.js

js aisummitbudapest.hu collected 2026-10-07 20:49:13 UTC 62,604 bytes, 1,535 lines download raw bytes

1let customModal = document.getElementById('custom-modal');
2let body = document.body;
3let scrollPosition;
4
5function explodeString(inputString, by = '#') {
6    // Split the string by the '#' character
7    const parts = inputString.split(by);
8    // Remove any empty strings in the result
9    return parts.filter(part => part.length > 0);
10}
11
12function openModal() {
13    scrollPosition = window.scrollY
14    body.style.position = 'fixed';
15    body.style.width = '100%'
16    body.style.top = `-${scrollPosition}px`;
17    customModal.style.display = 'block';
18}
19
20function closeModal() {
21    customModal.style.display = 'none';
22    body.style.position = '';
23    body.style.top = '';
24    window.scrollTo(0, scrollPosition);
25}
26
27function bsTargetHref() {
28    window.location.href = this.dataset.bsTarget;
29}
30function handleClick(event) {
31    /*    event.preventDefault();
32        event.stopImmediatePropagation();*/
33}
34function changeAgendaSelect( tabid ) {
35    console.log(tabid);
36    let actOption = document.querySelector(".select_location option[data-tabid='" + tabid + "']");
37    actOption.selected = true;
38    window.location.hash = tabid;
39}
40
41function showAgendaTab(hash = null) {
42
43    if (hash === null && document.querySelector('[data-agenda-view].agenda-tag-results-active')) {
44        return;
45    }
46
47    if ( hash === null ) {
48        hash = window.location.hash;
49    }
50
51    let hash_array = explodeString(hash);
52
53    if ( hash_array.length > 1 ) {
54        setTimeout(() => {
55            const targetElement = document.getElementById(hash_array[1]);
56            if (targetElement) {
57                // Scroll to the element
58                targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
59            }
60        }, 1000);
61    }
62    let position = hash.search(/___/);
63    if ( position !== -1 ) {
64        const fragments = hash.split("___");
65        console.log(fragments);
66        let tabElement = document.getElementById(fragments[0].replace("#","") + '-tab')
67        if ( tabElement !== undefined && tabElement !== null ) {
68            let tab = new bootstrap.Tab(tabElement);
69            tab.show();
70        }
71         setTimeout(() => {
72             let tabElement2 = document.getElementById(hash + '-tab')
73             if ( tabElement2 !== undefined && tabElement !== null ) {
74                 const left_button = document.getElementById(hash_array[0]+'-tab');
75                 if ( left_button !== undefined && left_button !== null ) {
76                     left_button.click();
77                 }
78             }
79         }, 100);
80
81        if ( hash_array[1] !== undefined && hash_array[1] !== null ) {
82
83            setTimeout(() => {
84                const targetElement = document.getElementById(hash_array[1]);
85                if (targetElement) {
86                    // Scroll to the element
87                    targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
88                }
89            }, 1500);
90        }
91
92    } else {
93        let tabElement = document.querySelector(hash + '-tab')
94        if ( tabElement !== undefined && tabElement !== null ) {
95            let tab = new bootstrap.Tab(tabElement);
96            tab.show();
97        }
98    }
99}
100
101$(function () {
102    if (location.hash) {
103        let tabElement = document.querySelector(location.hash + '-tab')
104        let tab = new bootstrap.Tab(tabElement);
105        tab.show();
106    }
107});
108
109$(function () {
110    $( window ).on( "load", function(e) {
111        showAgendaTab();
112    } );
113
114    if (location.hash) {
115        let tabElement = document.querySelector(location.hash + '-tab')
116        let tab = new bootstrap.Tab(tabElement);
117        tab.show();
118    }
119
120    document.querySelectorAll('.select_location').forEach(function (el) {
121        const location_select = el;
122        if ( location_select !== undefined && location_select !== null ) {
123            location_select.addEventListener("change", function(e) {
124                const actId = location_select.options[location_select.selectedIndex].dataset.tabid;
125                showAgendaTab( actId );
126            });
127        }
128    });
129
130
131
132});
133// Figyeli az URL hash változását
134window.addEventListener("hashchange", function() {
135    showAgendaTab();
136});
137
138/**
139 * Keep the visitor's room choice when they switch between agenda days.
140 * Room IDs are stable across days, while a room can be absent from an
141 * individual day's program. In that case the first room is shown temporarily
142 * and the saved choice remains available when a later day contains it again.
143 */
144(function preserveAgendaRoomAcrossDays() {
145    'use strict';
146
147    const views = document.querySelectorAll('[data-agenda-view].multi_days');
148
149    views.forEach((view) => {
150        let preferredRoomId = null;
151        const automaticRoomTabs = new WeakSet();
152
153        const roomTabsIn = (dayPane) => Array.from(
154            dayPane.querySelectorAll('.nav-rooms button[data-agenda-room-id]')
155        );
156
157        const syncRoomSelect = (dayPane, roomId) => {
158            const select = dayPane.querySelector('.select_location');
159            if (!select) {
160                return;
161            }
162
163            const matchingOption = Array.from(select.options).find((option) =>
164                option.dataset.agendaRoomId === roomId
165            );
166            if (matchingOption) {
167                select.selectedIndex = matchingOption.index;
168            }
169        };
170
171        const showRoom = (dayPane, roomId) => {
172            const roomTabs = roomTabsIn(dayPane);
173            if (roomTabs.length === 0) {
174                return;
175            }
176
177            const roomTab = roomTabs.find((tab) => tab.dataset.agendaRoomId === roomId)
178                || roomTabs[0];
179            syncRoomSelect(dayPane, roomTab.dataset.agendaRoomId);
180
181            if (!roomTab.classList.contains('active')) {
182                automaticRoomTabs.add(roomTab);
183                bootstrap.Tab.getOrCreateInstance(roomTab).show();
184            }
185        };
186
187        const initialDay = view.querySelector('.tab-pane.active');
188        const initialRoomTab = initialDay?.querySelector(
189            '.nav-rooms button.active[data-agenda-room-id]'
190        ) || view.querySelector('.nav-rooms button[data-agenda-room-id]');
191        if (initialRoomTab) {
192            preferredRoomId = initialRoomTab.dataset.agendaRoomId;
193        }
194
195        view.addEventListener('click', (event) => {
196            const roomTab = event.target.closest('.nav-rooms button[data-agenda-room-id]');
197            if (roomTab && view.contains(roomTab)) {
198                preferredRoomId = roomTab.dataset.agendaRoomId;
199            }
200        });
201
202        view.addEventListener('change', (event) => {
203            if (!event.target.matches('.select_location')) {
204                return;
205            }
206
207            const selectedOption = event.target.options[event.target.selectedIndex];
208            if (selectedOption?.dataset.agendaRoomId) {
209                preferredRoomId = selectedOption.dataset.agendaRoomId;
210            }
211        });
212
213        view.addEventListener('shown.bs.tab', (event) => {
214            const tab = event.target;
215
216            if (tab.matches('[data-agenda-day-tab]')) {
217                const target = tab.getAttribute('data-bs-target') || '';
218                const dayPane = target.startsWith('#')
219                    ? document.getElementById(target.slice(1))
220                    : null;
221                if (dayPane && view.contains(dayPane)) {
222                    showRoom(dayPane, preferredRoomId);
223                }
224                return;
225            }
226
227            if (!tab.matches('.nav-rooms button[data-agenda-room-id]')) {
228                return;
229            }
230
231            if (automaticRoomTabs.has(tab)) {
232                automaticRoomTabs.delete(tab);
233                return;
234            }
235
236            preferredRoomId = tab.dataset.agendaRoomId;
237        });
238    });
239})();
240
241// Ha az oldal betöltésekor is szeretnéd lefuttatni a függvényt, ha van hash az URL-ben
242if (window.location.hash) {
243    showAgendaTab();
244}
245
246/**
247 * Public agenda booking adapter.
248 *
249 * This deliberately does not use the legacy `app.ajaxLoadPage` helper: that
250 * helper assumes a 200 HTML response and cannot preserve structured 400/429
251 * envelopes or refreshed CSRF fields. The delegated listener also survives
252 * the tag filter moving agenda items between containers.
253 */
254(function agendaBookingAdapter(window, document) {
255    'use strict';
256
257    const HASH_PATTERN = /^[A-Z0-9]{12}$/;
258
259    const state = {
260        modal: null,
261        trigger: null,
262        url: null,
263        csrfName: null,
264        csrfValue: null,
265        captchaAction: 'agenda_booking',
266        captchaSiteKey: null,
267        translations: {},
268        returnFocus: null,
269        rateLimitTimer: null,
270        rateLimitUntil: 0,
271        availability: null,
272        canSubmit: false,
273        loadSequence: 0,
274        selectedTicketHash: null,
275        defaultTitle: '',
276    };
277
278    const by = (selector) => state.modal?.querySelector(selector) || null;
279    const form = () => by('[data-agenda-booking-form]');
280    const error = () => by('[data-agenda-booking-error]');
281    const success = () => by('[data-agenda-booking-success]');
282
283    const setVisible = (element, visible) => {
284        if (!element) return;
285        element.classList.toggle('d-none', !visible);
286    };
287
288    const setMessage = (element, message) => {
289        if (!element) return;
290        element.textContent = message || '';
291        setVisible(element, Boolean(message));
292    };
293
294    const setInvalid = (isInvalid) => {
295        const hashField = by('[data-agenda-booking-hash]');
296        hashField?.setAttribute('aria-invalid', isInvalid ? 'true' : 'false');
297    };
298
299    const setLoading = (isLoading) => {
300        setVisible(by('[data-agenda-booking-loading]'), isLoading);
301        setVisible(by('[data-agenda-booking-content]'), !isLoading);
302        const submit = by('[data-agenda-booking-submit]');
303        if (submit) {
304            submit.disabled = isLoading || !state.canSubmit || Boolean(state.rateLimitUntil);
305        }
306    };
307
308    /**
309     * Surfaces the in-flight POST beyond the disabled button: a spinner and a
310     * relabeled button so a slow network still reads as "working", not stuck.
311     */
312    const setSubmitting = (isSubmitting) => {
313        const submitButton = by('[data-agenda-booking-submit]');
314        const spinner = by('[data-agenda-booking-submit-spinner]');
315        const label = by('[data-agenda-booking-submit-label]');
316        if (submitButton) {
317            submitButton.disabled = isSubmitting || !state.canSubmit || Boolean(state.rateLimitUntil);
318            submitButton.setAttribute('aria-busy', isSubmitting ? 'true' : 'false');
319        }
320        setVisible(spinner, isSubmitting);
321        if (label) label.textContent = isSubmitting ? translate('loading') : translate('submit');
322    };
323
324    const loadTranslations = () => {
325        const raw = state.modal?.dataset.agendaBookingTranslations || '{}';
326        try {
327            const translations = JSON.parse(raw);
328            state.translations = translations && typeof translations === 'object' ? translations : {};
329        } catch (e) {
330            state.translations = {};
331        }
332    };
333
334    const translate = (key) => String(state.translations[key] || '');
335
336    const translateCount = (key, count) => translate(key).replace('{count}', String(count));
337
338    const normalizeTicketHash = (value) => String(value ?? '').trim().toUpperCase();
339
340    const setTicketPickerVisibility = (visible) => {
341        const picker = by('[data-agenda-booking-ticket-selector], [data-agenda-booking-ticket-picker]');
342        if (!picker) return;
343        picker.hidden = !visible;
344        picker.classList.toggle('d-none', !visible);
345    };
346
347    const setTicketEmptyVisibility = (visible) => {
348        const empty = by('[data-agenda-booking-ticket-empty]');
349        if (!empty) return;
350        empty.hidden = !visible;
351        empty.classList.toggle('d-none', !visible);
352    };
353
354    const resetTicketPicker = () => {
355        state.selectedTicketHash = null;
356        const list = by('[data-agenda-booking-ticket-list]');
357        if (list) list.replaceChildren();
358        setTicketEmptyVisibility(false);
359        setTicketPickerVisibility(false);
360        const status = by('[data-agenda-booking-ticket-status]');
361        if (status) status.textContent = '';
362    };
363
364    const appendTicketField = (button, className, labelKey, value) => {
365        const row = document.createElement('span');
366        row.className = `agenda-booking-ticket-field ${className}`;
367
368        const label = document.createElement('span');
369        label.className = 'agenda-booking-ticket-label';
370        label.textContent = translate(labelKey);
371        row.appendChild(label);
372
373        const text = document.createElement('span');
374        text.className = 'agenda-booking-ticket-value';
375        text.textContent = value;
376        row.appendChild(text);
377        button.appendChild(row);
378    };
379
380    const renderUserTickets = (loggedIn, tickets) => {
381        resetTicketPicker();
382        if (loggedIn !== true) return;
383
384        setTicketPickerVisibility(true);
385        const list = by('[data-agenda-booking-ticket-list]');
386        if (!list || !Array.isArray(tickets)) {
387            setTicketEmptyVisibility(true);
388            return;
389        }
390
391        const fragment = document.createDocumentFragment();
392        tickets.forEach((ticket) => {
393            if (!ticket || typeof ticket !== 'object') return;
394            const hash = normalizeTicketHash(ticket.ticket_hash);
395            if (!HASH_PATTERN.test(hash)) return;
396
397            const name = String(ticket.name ?? '');
398            const email = String(ticket.email ?? '');
399            const button = document.createElement('button');
400            button.type = 'button';
401            button.className = 'agenda-booking-ticket';
402            button.dataset.agendaBookingTicket = '1';
403            button.dataset.agendaBookingTicketItem = '1';
404            button.dataset.ticketHash = hash;
405            button.setAttribute('aria-label', [
406                [translate('ticket_name'), name],
407                [translate('ticket_email'), email],
408                [translate('ticket_code'), hash],
409            ]
410                .filter(([, value]) => value !== '')
411                .map(([label, value]) => `${label}: ${value}`)
412                .join(', '));
413            appendTicketField(button, 'agenda-booking-ticket-name', 'ticket_name', name);
414            appendTicketField(button, 'agenda-booking-ticket-email', 'ticket_email', email);
415            appendTicketField(button, 'agenda-booking-ticket-code', 'ticket_code', hash);
416            fragment.appendChild(button);
417        });
418
419        list.replaceChildren(fragment);
420        const hasTickets = list.children.length > 0;
421        setTicketEmptyVisibility(!hasTickets);
422    };
423
424    const renderUserBookings = (bookings) => {
425        const section = by('[data-agenda-booking-owned]');
426        const list = by('[data-agenda-booking-owned-list]');
427        if (!section || !list) return;
428        list.replaceChildren();
429        bookings.forEach((booking) => {
430            const row = document.createElement('div');
431            row.className = 'agenda-booking-owned-row';
432            const description = document.createElement('span');
433            description.textContent = [booking.name, booking.email, booking.ticket_hash]
434                .filter(Boolean).join(' · ');
435            const cancel = document.createElement('button');
436            cancel.type = 'button';
437            cancel.className = 'btn btn-outline-danger btn-sm';
438            cancel.dataset.agendaBookingCancel = String(booking.booking_id);
439            cancel.textContent = translate('cancel_booking');
440            row.append(description, cancel);
441            list.appendChild(row);
442        });
443        section.hidden = bookings.length === 0;
444        section.classList.toggle('d-none', bookings.length === 0);
445    };
446
447    const selectTicket = (ticketButton) => {
448        const hashField = by('[data-agenda-booking-hash]');
449        const hash = normalizeTicketHash(ticketButton?.dataset?.ticketHash);
450        if (!hashField || !HASH_PATTERN.test(hash)) return;
451
452        state.selectedTicketHash = hash;
453        hashField.value = hash;
454        setMessage(error(), '');
455        setMessage(success(), '');
456        setInvalid(false);
457
458        const status = by('[data-agenda-booking-ticket-status]');
459        if (status) status.textContent = translate('ticket_selected');
460        hashField.focus();
461    };
462
463    const updateCsrf = (data) => {
464        if (!data || typeof data !== 'object') return;
465        const name = data.csrf_token_name || data.csrf_name || null;
466        const value = data.csrf_token_value || data.csrf_hash || data.csrf || null;
467        if (name) state.csrfName = name;
468        if (value) state.csrfValue = value;
469        const csrf = by('[data-agenda-booking-csrf]');
470        if (csrf) {
471            csrf.name = state.csrfName || csrf.name || 'csrf_token';
472            csrf.value = state.csrfValue || '';
473        }
474    };
475
476    const parseResponse = async (response) => {
477        let payload = null;
478        try {
479            payload = await response.json();
480        } catch (e) {
481            payload = { result: false, code: 'TECHNICAL_ERROR', message: translate('invalid_response'), data: null };
482        }
483        updateCsrf(payload?.data);
484        return { response, payload };
485    };
486
487    const availabilityText = (availability) => {
488        if (!availability) return '';
489        const status = availability.availability_status || 'disabled';
490        if (['full', 'not_open', 'closed', 'disabled'].includes(status)) return translate(status);
491        if (availability.capacity_unlimited) return translate('unlimited');
492        if (availability.show_remaining_booking && Number.isFinite(Number(availability.remaining_places_booking))) {
493            return translateCount('remaining', availability.remaining_places_booking);
494        }
495        return '';
496    };
497
498    /**
499     * Normalize the small availability envelope returned by GET/POST.  A
500     * successful booking can consume the final place while a stale response
501     * still says `available`; treating a finite zero count as full keeps the
502     * modal and the agenda list in sync until the next page refresh.
503     */
504    const normalizeAvailability = (availability, fallbackStatus = null) => {
505        const normalized = availability && typeof availability === 'object'
506            ? { ...availability }
507            : {};
508        let status = normalized.availability_status || fallbackStatus || 'disabled';
509        const rawRemaining = Object.prototype.hasOwnProperty.call(normalized, 'remaining_places_booking')
510            && normalized.remaining_places_booking !== null
511            && normalized.remaining_places_booking !== undefined
512            ? normalized.remaining_places_booking
513            : normalized.remaining_places_list;
514        const remaining = rawRemaining === null || rawRemaining === undefined || rawRemaining === ''
515            ? Number.NaN
516            : Number(rawRemaining);
517        if (
518            status === 'available'
519            && !Boolean(normalized.capacity_unlimited)
520            && Number.isFinite(remaining)
521            && remaining <= 0
522        ) {
523            status = 'full';
524        }
525        normalized.availability_status = status;
526        return normalized;
527    };
528
529    const applyModalAvailability = (availability) => {
530        const previous = state.availability || {};
531        const incoming = availability && typeof availability === 'object' ? availability : {};
532        const normalized = normalizeAvailability({
533            ...incoming,
534            // POST booking responses intentionally expose a small allowlisted
535            // envelope.  Preserve GET-only presentation flags while never
536            // carrying a stale remaining count into the next state.
537            ...(Object.prototype.hasOwnProperty.call(incoming, 'enabled')
538                ? {}
539                : { enabled: previous.enabled }),
540            ...(Object.prototype.hasOwnProperty.call(incoming, 'capacity_unlimited')
541                ? {}
542                : { capacity_unlimited: previous.capacity_unlimited }),
543            ...(Object.prototype.hasOwnProperty.call(incoming, 'show_remaining_booking')
544                ? {}
545                : { show_remaining_booking: previous.show_remaining_booking }),
546            ...(Object.prototype.hasOwnProperty.call(incoming, 'show_full')
547                ? {}
548                : { show_full: previous.show_full }),
549            ...(Object.prototype.hasOwnProperty.call(incoming, 'availability_status')
550                ? {}
551                : { availability_status: previous.availability_status }),
552        }, previous.availability_status || null);
553        state.availability = normalized;
554        state.canSubmit = normalized.availability_status === 'available'
555            && normalized.enabled !== false;
556
557        const availabilityElement = by('[data-agenda-booking-availability]');
558        if (availabilityElement) {
559            availabilityElement.textContent = availabilityText(normalized);
560        }
561
562        const submitButton = by('[data-agenda-booking-submit]');
563        if (submitButton) {
564            submitButton.disabled = !state.canSubmit || Boolean(state.rateLimitUntil);
565        }
566        return normalized;
567    };
568
569    const statusMessageKey = (container, availability) => {
570        const status = availability.availability_status || 'disabled';
571        if (status !== 'full') return status;
572
573        // `show_full` is an explicit presentation switch.  Keep the generic
574        // unavailable label when the list was configured not to expose a
575        // separate "full" marker.
576        const showFull = Object.prototype.hasOwnProperty.call(availability, 'show_full')
577            ? Boolean(availability.show_full)
578            : container?.dataset.agendaBookingShowFull === '1';
579        return showFull ? 'full' : 'disabled';
580    };
581
582    const updateTriggers = (agendaId, data) => {
583        if (!agendaId || !data) return;
584        const escapedAgendaId = window.CSS?.escape
585            ? window.CSS.escape(String(agendaId))
586            : String(agendaId).replace(/(["\\])/g, '\\$1');
587        document.querySelectorAll(`[data-agenda-booking-id="${escapedAgendaId}"]`).forEach((container) => {
588            const normalized = normalizeAvailability(
589                data,
590                container.dataset.agendaBookingStatus || null
591            );
592            const remaining = container.querySelector('[data-agenda-booking-remaining]');
593            const message = container.querySelector('[data-agenda-booking-status-message]');
594            const hasListCount = Object.prototype.hasOwnProperty.call(data, 'remaining_places_list');
595            const hasBookingCount = Object.prototype.hasOwnProperty.call(data, 'remaining_places_booking');
596            if (remaining && (hasListCount || hasBookingCount)) {
597                // The public POST adapter may expose both counts.  The list
598                // switch is independent from the modal switch, so prefer the
599                // explicit list count and fall back to the booking count for
600                // older responses.
601                const value = hasListCount ? data.remaining_places_list : data.remaining_places_booking;
602                if (value === null || value === undefined) {
603                    remaining.textContent = '';
604                } else {
605                    remaining.textContent = translateCount('remaining', value);
606                }
607            }
608            const status = normalized.availability_status;
609            container.dataset.agendaBookingStatus = status;
610            const detailState = container.querySelector('.agenda-detail-booking-state');
611            if (detailState) {
612                detailState.textContent = translate(status === 'available'
613                    ? 'available' : statusMessageKey(container, normalized));
614            }
615            if (Object.prototype.hasOwnProperty.call(data, 'user_has_booking')) {
616                container.dataset.agendaUserBooked = data.user_has_booking ? '1' : '0';
617            }
618            const userBooked = container.dataset.agendaUserBooked === '1';
619            const trigger = container.querySelector('[data-agenda-booking-trigger]');
620            if (trigger) trigger.textContent = translate(userBooked ? 'button_with_cancel' : 'button');
621            if ((status === 'available' && normalized.enabled !== false) || userBooked) {
622                trigger?.classList.remove('d-none');
623                if (trigger) {
624                    trigger.hidden = false;
625                    trigger.disabled = false;
626                }
627                if (message) {
628                    message.hidden = true;
629                    message.classList.add('d-none');
630                }
631                return;
632            }
633
634            if (trigger) {
635                // The server renders a status-only state for non-bookable
636                // agendas.  Mirror that state after a race/full response.
637                trigger.hidden = true;
638                trigger.disabled = true;
639                trigger.classList.add('d-none');
640            }
641            if (message) {
642                const key = statusMessageKey(container, normalized);
643                message.textContent = translate(key) || availabilityText({
644                    ...normalized,
645                    availability_status: key,
646                });
647                message.dataset.agendaBookingStatusKey = key;
648                message.hidden = false;
649                message.classList.remove('d-none');
650            }
651        });
652    };
653
654    const refreshBookingState = async (preserveAvailability = false) => {
655        const result = await parseResponse(await window.fetch(state.url, {
656            method: 'GET',
657            headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
658            credentials: 'same-origin',
659        }));
660        if (!result.response.ok || result.payload?.result !== true) {
661            throw new Error(result.payload?.message || translate('load_error'));
662        }
663        const data = result.payload.data || {};
664        const bookings = Array.isArray(data.user_bookings) ? data.user_bookings : [];
665        renderUserBookings(bookings);
666        const availability = preserveAvailability ? state.availability : data.availability;
667        if (!preserveAvailability) applyModalAvailability(availability);
668        updateTriggers(data.agenda_id, {
669            ...availability,
670            user_has_booking: bookings.length > 0,
671        });
672        updateCsrf(data);
673        return data;
674    };
675
676    const cancelBooking = async (button) => {
677        if (!state.url || button.disabled) return;
678        if (!window.confirm(translate('cancel_confirm'))) return;
679        button.disabled = true;
680        setMessage(error(), '');
681        setMessage(success(), '');
682        try {
683            const body = new FormData();
684            if (state.csrfName && state.csrfValue) body.set(state.csrfName, state.csrfValue);
685            const result = await parseResponse(await window.fetch(
686                `${state.url}/bookings/${encodeURIComponent(button.dataset.agendaBookingCancel)}/cancel`, {
687                    method: 'POST', body,
688                    headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
689                    credentials: 'same-origin',
690                }
691            ));
692            if (!result.response.ok || result.payload?.result !== true) {
693                throw new Error(result.payload?.message || translate('failed'));
694            }
695            await refreshBookingState();
696            setMessage(success(), result.payload.message || translate('cancel_success'));
697        } catch (exception) {
698            button.disabled = false;
699            setMessage(error(), exception.message || translate('failed'));
700        }
701    };
702
703    const showModal = () => {
704        if (!state.modal) return;
705        if (window.bootstrap?.Modal) {
706            window.bootstrap.Modal.getOrCreateInstance(state.modal).show();
707        } else {
708            state.modal.classList.add('show');
709            state.modal.style.display = 'block';
710            state.modal.removeAttribute('aria-hidden');
711        }
712    };
713
714    const hideModal = () => {
715        if (!state.modal) return;
716        if (window.bootstrap?.Modal) {
717            window.bootstrap.Modal.getOrCreateInstance(state.modal).hide();
718        } else {
719            state.modal.classList.remove('show');
720            state.modal.style.display = 'none';
721            state.modal.setAttribute('aria-hidden', 'true');
722        }
723    };
724
725    const loadModal = async (trigger) => {
726        state.modal = document.querySelector('[data-agenda-booking-modal]');
727        if (!state.modal) return;
728        clearRateLimitCountdown();
729        const requestSequence = ++state.loadSequence;
730        state.availability = null;
731        state.canSubmit = false;
732        state.csrfName = null;
733        state.csrfValue = null;
734        state.captchaAction = 'agenda_booking';
735        state.captchaSiteKey = null;
736        loadTranslations();
737        resetTicketPicker();
738        renderUserBookings([]);
739        state.trigger = trigger;
740        state.returnFocus = trigger;
741        state.url = trigger.dataset.agendaBookingUrl;
742        if (!state.url) return;
743        setMessage(error(), '');
744        setMessage(success(), '');
745        setInvalid(false);
746        const hash = by('[data-agenda-booking-hash]');
747        if (hash) hash.value = '';
748        const title = by('[data-agenda-booking-title]');
749        if (title) title.textContent = translate('title') || state.defaultTitle;
750        const info = by('[data-agenda-booking-info]');
751        if (info) info.replaceChildren();
752        setLoading(true);
753        showModal();
754        try {
755            const result = await parseResponse(await window.fetch(state.url, {
756                method: 'GET',
757                headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
758                credentials: 'same-origin',
759            }));
760            if (requestSequence !== state.loadSequence) return;
761            if (!result.response.ok || result.payload?.result !== true) {
762                throw new Error(result.payload?.message || translate('load_error'));
763            }
764            const data = result.payload.data || {};
765            state.captchaAction = data.captcha_action || 'agenda_booking';
766            state.captchaSiteKey = data.captcha_site_key || null;
767            if (title && data.title) title.textContent = data.title;
768            if (info) info.innerHTML = data.booking_info_html || '';
769            renderUserTickets(
770                data.user_logged_in === true,
771                Array.isArray(data.user_tickets) ? data.user_tickets : []
772            );
773            renderUserBookings(Array.isArray(data.user_bookings) ? data.user_bookings : []);
774            applyModalAvailability(data.availability);
775            updateTriggers(data.agenda_id, {
776                ...data.availability,
777                user_has_booking: Array.isArray(data.user_bookings) && data.user_bookings.length > 0,
778            });
779            if (hash) hash.value = '';
780            updateCsrf(data);
781            setLoading(false);
782            window.setTimeout(() => hash?.focus(), 0);
783        } catch (exception) {
784            if (requestSequence !== state.loadSequence) return;
785            resetTicketPicker();
786            if (hash) hash.value = '';
787            setInvalid(false);
788            state.availability = null;
789            state.canSubmit = false;
790            setLoading(false);
791            setMessage(error(), exception.message || translate('load_error'));
792        }
793    };
794
795    const executeCaptcha = async () => {
796        if (!window.grecaptcha?.enterprise?.execute || !state.captchaSiteKey) return '';
797        return window.grecaptcha.enterprise.execute(state.captchaSiteKey, { action: state.captchaAction });
798    };
799
800    const clearRateLimitCountdown = () => {
801        if (state.rateLimitTimer) {
802            window.clearInterval(state.rateLimitTimer);
803            state.rateLimitTimer = null;
804        }
805        state.rateLimitUntil = 0;
806    };
807
808    const startRateLimitCountdown = (seconds) => {
809        clearRateLimitCountdown();
810        const submitButton = by('[data-agenda-booking-submit]');
811        state.rateLimitUntil = Date.now() + Math.max(0, seconds) * 1000;
812        if (submitButton) submitButton.disabled = true;
813
814        const tick = () => {
815            const remaining = Math.max(0, Math.ceil((state.rateLimitUntil - Date.now()) / 1000));
816            if (remaining <= 0) {
817                clearRateLimitCountdown();
818                if (submitButton) submitButton.disabled = !state.canSubmit;
819                setMessage(error(), '');
820                return;
821            }
822            setMessage(error(), translateCount('rate_limited_countdown', remaining));
823        };
824
825        tick();
826        state.rateLimitTimer = window.setInterval(tick, 1000);
827    };
828
829    const submit = async (event) => {
830        event.preventDefault();
831        if (!state.url || !state.modal) return;
832        const activeForm = form();
833        if (!activeForm || activeForm.dataset.submitting === '1') return;
834        const hash = by('[data-agenda-booking-hash]');
835        // Clear any result from a previous attempt before evaluating this one,
836        // otherwise a stale success/error alert stays stacked above the new
837        // message on every early-return validation branch below.
838        setMessage(error(), '');
839        setMessage(success(), '');
840        setInvalid(false);
841        if (state.availability && !state.canSubmit) {
842            setMessage(error(), availabilityText(state.availability) || translate('failed'));
843            return;
844        }
845        const trimmedHash = (hash?.value || '').trim().toUpperCase();
846        if (!trimmedHash) {
847            setMessage(error(), translate('ticket_required'));
848            setInvalid(true);
849            hash?.focus();
850            return;
851        }
852        if (!HASH_PATTERN.test(trimmedHash)) {
853            setMessage(error(), translate('invalid_hash_format'));
854            setInvalid(true);
855            hash?.focus();
856            return;
857        }
858        activeForm.dataset.submitting = '1';
859        setSubmitting(true);
860        try {
861            const captchaToken = await executeCaptcha().catch(() => '');
862            const body = new FormData(activeForm);
863            body.set('ticket_hash', trimmedHash);
864            body.set('captcha_token', captchaToken || '');
865            if (state.csrfName && state.csrfValue) body.set(state.csrfName, state.csrfValue);
866            const result = await parseResponse(await window.fetch(state.url, {
867                method: 'POST',
868                body,
869                headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
870                credentials: 'same-origin',
871            }));
872            const payload = result.payload || {};
873            if (!result.response.ok || payload.result !== true) {
874                throw Object.assign(new Error(payload.message || translate('failed')), { code: payload.code, payload });
875            }
876            const data = payload.data || {};
877            applyModalAvailability(data);
878            updateTriggers(data.agenda_id || state.trigger?.dataset.agendaId, data);
879            const emailStatus = data.email_status;
880            const emailStatusMessage = emailStatus ? translate(`email_${emailStatus}`) : '';
881            const suffix = emailStatusMessage ? ` (${emailStatusMessage})` : '';
882            setMessage(success(), `${payload.message || translate('success')}${suffix}`);
883            activeForm.reset();
884            if (hash) hash.value = '';
885            try {
886                await refreshBookingState(true);
887            } catch (refreshError) {
888                // The booking succeeded; the list can be refreshed when reopened.
889            }
890        } catch (exception) {
891            if (exception.code === 'RATE_LIMITED') {
892                const retryAfter = Number(exception.payload?.data?.retry_after_seconds);
893                startRateLimitCountdown(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : 300);
894            } else {
895                const failureData = exception.payload?.data || {};
896                const failureCode = String(exception.code || '').toLowerCase();
897                if (failureCode === 'booking_full') {
898                    // A race can fill the slot between opening the modal and
899                    // submitting.  Prefer the stable code over a stale or
900                    // misleading server message (e.g. "invalid HASH") and
901                    // lock this modal session immediately.
902                    const fullAvailability = applyModalAvailability({
903                        ...failureData,
904                        availability_status: 'full',
905                        agenda_id: failureData.agenda_id || state.trigger?.dataset.agendaId,
906                    });
907                    updateTriggers(
908                        fullAvailability.agenda_id || state.trigger?.dataset.agendaId,
909                        fullAvailability
910                    );
911                }
912                const messageKey = {
913                    booking_full: 'full',
914                    booking_closed: 'closed',
915                    booking_not_open: 'not_open',
916                    captcha_failed: 'captcha_failed',
917                    invalid_ticket: 'invalid_ticket',
918                    ticket_not_eligible: 'not_eligible',
919                }[failureCode];
920                if (failureCode === 'invalid_ticket' || failureCode === 'ticket_not_eligible') {
921                    setInvalid(true);
922                }
923                setMessage(
924                    error(),
925                    (messageKey && translate(messageKey)) || exception.message || translate('failed')
926                );
927            }
928        } finally {
929            activeForm.dataset.submitting = '0';
930            setSubmitting(false);
931        }
932    };
933
934    const init = () => {
935        const modal = document.querySelector('[data-agenda-booking-modal]');
936        if (!modal || modal.dataset.agendaBookingInitialized === '1') return;
937        modal.dataset.agendaBookingInitialized = '1';
938        state.modal = modal;
939        state.defaultTitle = modal.querySelector('[data-agenda-booking-title]')?.textContent || '';
940        modal.addEventListener('submit', submit);
941        modal.addEventListener('click', (event) => {
942            const cancelButton = event.target.closest?.('[data-agenda-booking-cancel]');
943            if (cancelButton && modal.contains(cancelButton)) {
944                event.preventDefault();
945                cancelBooking(cancelButton);
946                return;
947            }
948            const ticketButton = event.target.closest?.('[data-agenda-booking-ticket]');
949            if (!ticketButton || !modal.contains(ticketButton)) return;
950            event.preventDefault();
951            selectTicket(ticketButton);
952        });
953        modal.addEventListener('shown.bs.modal', () => by('[data-agenda-booking-hash]')?.focus());
954        modal.addEventListener('hidden.bs.modal', () => {
955            state.loadSequence += 1;
956            clearRateLimitCountdown();
957            state.availability = null;
958            state.canSubmit = false;
959            state.csrfName = null;
960            state.csrfValue = null;
961            state.captchaSiteKey = null;
962            resetTicketPicker();
963            const hash = by('[data-agenda-booking-hash]');
964            if (hash) hash.value = '';
965            setMessage(error(), '');
966            setMessage(success(), '');
967            setInvalid(false);
968            state.returnFocus?.focus?.();
969        });
970        document.addEventListener('click', (event) => {
971            const trigger = event.target.closest?.('[data-agenda-booking-trigger]');
972            if (!trigger) return;
973            event.preventDefault();
974            loadModal(trigger);
975        });
976    };
977
978    window.agendaBookingAdapter = { init, loadModal, submit, updateCsrf };
979    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
980    else init();
981})(window, document);
982
983function initAgendaTagFilter() {
984    const filter = document.getElementById('agenda-tag-filter');
985    if (!filter || filter.dataset.initialized === '1') {
986        return;
987    }
988    filter.dataset.initialized = '1';
989
990    const languageCheckboxes = Array.from(filter.querySelectorAll('.agenda-language-filter-checkbox'));
991    const typeCheckboxes = Array.from(filter.querySelectorAll('.agenda-type-filter-checkbox'));
992    const tagCheckboxes = Array.from(filter.querySelectorAll('.agenda-tag-filter-checkbox'));
993    const checkboxes = [...languageCheckboxes, ...typeCheckboxes, ...tagCheckboxes];
994    const agendaView = document.querySelector('[data-agenda-view]');
995    const crossRoomEnabled = filter.dataset.crossRoomEnabled === '1'
996        && Boolean(agendaView?.querySelector('[data-agenda-tag-results]'));
997    const results = crossRoomEnabled ? agendaView.querySelector('[data-agenda-tag-results]') : null;
998    const items = Array.from((crossRoomEnabled ? agendaView : document).querySelectorAll('.agenda-item[data-agenda-tag-ids]'));
999    const empty = document.getElementById('agenda-tag-filter-empty');
1000    const clear = document.getElementById('agenda-tag-filter-clear');
1001    const count = document.getElementById('agenda-tag-filter-count');
1002    const resultList = results?.querySelector('[data-agenda-tag-results-list]');
1003    const resultSummary = results?.querySelector('[data-agenda-tag-results-summary]');
1004    const resultEmpty = results?.querySelector('[data-agenda-tag-results-empty]');
1005    const itemRecords = items.map((item) => ({
1006        item,
1007        placeholder: document.createComment(`agenda-tag-filter:${item.id}`),
1008    }));
1009    let resultsMode = false;
1010    let savedAgendaHash = null;
1011
1012    const getContrastTextColor = (hex) => {
1013        const valid = /^#[0-9a-fA-F]{6}$/.test(hex || '') ? hex : '#3B82F6';
1014        const channels = [1, 3, 5].map((i) => parseInt(valid.substr(i, 2), 16));
1015        const weights = [0.2126, 0.7152, 0.0722];
1016        let luminance = 0;
1017        channels.forEach((channel, index) => {
1018            const normalized = channel / 255;
1019            const linear = normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
1020            luminance += linear * weights[index];
1021        });
1022        const blackContrast = (luminance + 0.05) / 0.05;
1023        const whiteContrast = 1.05 / (luminance + 0.05);
1024        return blackContrast >= whiteContrast ? '#000000' : '#ffffff';
1025    };
1026
1027    const setStatusVisibility = (element, visible) => {
1028        if (!element) {
1029            return;
1030        }
1031        element.hidden = !visible;
1032        element.classList.toggle('d-none', !visible);
1033    };
1034
1035    const setFilterControlVisibility = (element, visible) => {
1036        if (!element) {
1037            return;
1038        }
1039        element.hidden = !visible;
1040        element.classList.toggle('d-none', !visible);
1041    };
1042
1043    const setResultContextVisibility = (item, visible) => {
1044        const context = item.querySelector('[data-agenda-room-context]');
1045        if (context) {
1046            context.classList.toggle('d-none', !visible);
1047        }
1048    };
1049
1050    const ensurePlaceholders = () => {
1051        itemRecords.forEach(({ item, placeholder }) => {
1052            if (!placeholder.parentNode && item.parentNode) {
1053                item.parentNode.insertBefore(placeholder, item);
1054            }
1055        });
1056    };
1057
1058    const restoreMovedItems = (removePlaceholders = false) => {
1059        itemRecords.forEach(({ item, placeholder }) => {
1060            if (placeholder.parentNode) {
1061                if (item.parentNode !== placeholder.parentNode || placeholder.nextSibling !== item) {
1062                    placeholder.parentNode.insertBefore(item, placeholder.nextSibling);
1063                }
1064                setResultContextVisibility(item, false);
1065                if (removePlaceholders) {
1066                    placeholder.remove();
1067                }
1068            }
1069        });
1070    };
1071
1072    const clearResults = () => {
1073        if (resultList) {
1074            resultList.replaceChildren();
1075        }
1076    };
1077
1078    const setResultsMode = (enabled) => {
1079        if (!crossRoomEnabled || !results) {
1080            return;
1081        }
1082
1083        agendaView.classList.toggle('agenda-tag-results-active', enabled);
1084        agendaView.querySelectorAll('[data-agenda-normal-control], [data-agenda-normal-content]').forEach((element) => {
1085            element.hidden = enabled;
1086            element.setAttribute('aria-hidden', enabled ? 'true' : 'false');
1087        });
1088        results.hidden = !enabled;
1089        results.classList.toggle('d-none', !enabled);
1090        results.setAttribute('aria-hidden', enabled ? 'false' : 'true');
1091        resultsMode = enabled;
1092    };
1093
1094    const updateUrl = (selectedIds, selectedLanguages, selectedTypeIds, replace = false) => {
1095        const url = new URL(window.location.href);
1096        if (selectedIds.length > 0) {
1097            url.searchParams.set('tags', selectedIds.join(','));
1098        } else {
1099            url.searchParams.delete('tags');
1100        }
1101        url.searchParams.delete('languages');
1102        selectedLanguages.forEach((language) => {
1103            url.searchParams.append('languages', language);
1104        });
1105        if (selectedTypeIds.length > 0) {
1106            url.searchParams.set('types', selectedTypeIds.join(','));
1107        } else {
1108            url.searchParams.delete('types');
1109        }
1110        const method = replace ? 'replaceState' : 'pushState';
1111        window.history[method]({}, '', url.pathname + (url.search ? url.search : '') + url.hash);
1112    };
1113
1114    const replaceTemplate = (template, values) => String(template || '').replace(/\{(\w+)\}/g, (match, key) =>
1115        Object.prototype.hasOwnProperty.call(values, key) ? values[key] : match
1116    );
1117
1118    const numericDatasetValue = (item, key, fallback = 0) => {
1119        const value = Number(item.dataset[key]);
1120        return Number.isFinite(value) ? value : fallback;
1121    };
1122
1123    const getAgendaStartTimestamp = (item) => {
1124        if (item.dataset.agendaFullTime === '1') {
1125            return Number.NEGATIVE_INFINITY;
1126        }
1127
1128        const raw = item.dataset.agendaStartTime || '';
1129        const timestamp = Date.parse(raw.includes('T') ? raw : raw.replace(' ', 'T'));
1130        return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp;
1131    };
1132
1133    const compareAgendaItems = (left, right) => {
1134        const dayOrder = numericDatasetValue(left, 'agendaDayOrder') - numericDatasetValue(right, 'agendaDayOrder');
1135        if (dayOrder !== 0) {
1136            return dayOrder;
1137        }
1138
1139        const leftStartTime = getAgendaStartTimestamp(left);
1140        const rightStartTime = getAgendaStartTimestamp(right);
1141        if (leftStartTime !== rightStartTime) {
1142            if (leftStartTime === Number.NEGATIVE_INFINITY) {
1143                return -1;
1144            }
1145            if (rightStartTime === Number.NEGATIVE_INFINITY) {
1146                return 1;
1147            }
1148            return leftStartTime - rightStartTime;
1149        }
1150
1151        const roomOrder = numericDatasetValue(left, 'agendaRoomOrder') - numericDatasetValue(right, 'agendaRoomOrder');
1152        if (roomOrder !== 0) {
1153            return roomOrder;
1154        }
1155
1156        const agendaId = numericDatasetValue(left, 'agendaId', parseInt(left.id.replace('agenda-', ''), 10) || 0)
1157            - numericDatasetValue(right, 'agendaId', parseInt(right.id.replace('agenda-', ''), 10) || 0);
1158        if (agendaId !== 0) {
1159            return agendaId;
1160        }
1161
1162        return left.id.localeCompare(right.id);
1163    };
1164
1165    const renderTagResults = (matchingItems) => {
1166        if (!resultList) {
1167            return;
1168        }
1169
1170        clearResults();
1171        const groups = new Map();
1172        [...matchingItems].sort(compareAgendaItems).forEach((item) => {
1173            const dayId = item.dataset.agendaDayId || '__single_day__';
1174            if (!groups.has(dayId)) {
1175                const group = document.createElement('section');
1176                group.className = 'agenda-tag-results-day';
1177
1178                const dayName = (item.dataset.agendaDayName || '').trim();
1179                if (dayName) {
1180                    const dayTitle = document.createElement('h3');
1181                    dayTitle.className = 'agenda-tag-results-day-title';
1182                    dayTitle.textContent = replaceTemplate(results.dataset.resultsDayTemplate, { day: dayName });
1183                    group.appendChild(dayTitle);
1184                }
1185
1186                const dayList = document.createElement('div');
1187                dayList.className = 'agenda-tag-results-day-list';
1188                group.appendChild(dayList);
1189                resultList.appendChild(group);
1190                groups.set(dayId, dayList);
1191            }
1192
1193            setResultContextVisibility(item, true);
1194            groups.get(dayId).appendChild(item);
1195        });
1196    };
1197
1198    const updateResultSummary = (matchingItems) => {
1199        if (!resultSummary || !results) {
1200            return;
1201        }
1202
1203        const rooms = new Set(matchingItems.map((item) =>
1204            item.dataset.agendaRoomId || item.dataset.agendaRoomName || item.id
1205        ));
1206        resultSummary.textContent = replaceTemplate(results.dataset.resultsSummaryTemplate, {
1207            count: matchingItems.length,
1208            rooms: rooms.size,
1209        });
1210    };
1211
1212    const restoreAgendaState = () => {
1213        const hashToRestore = savedAgendaHash;
1214        savedAgendaHash = null;
1215        if (hashToRestore === null) {
1216            return;
1217        }
1218
1219        const url = new URL(window.location.href);
1220        url.hash = hashToRestore;
1221        window.history.replaceState({}, '', url.pathname + (url.search ? url.search : '') + url.hash);
1222        if (hashToRestore) {
1223            showAgendaTab(hashToRestore);
1224        }
1225    };
1226
1227    const normalizeLanguage = (language) => String(language || '').trim().toLocaleLowerCase();
1228
1229    const apply = (updateAddress = true) => {
1230        const selectedByGroup = new Map();
1231        const selectedIds = [];
1232        const selectedLanguages = [];
1233        const selectedTypeIds = [];
1234
1235        tagCheckboxes.forEach((checkbox) => {
1236            const label = checkbox.closest('.agenda-tag-filter-option');
1237            if (label) {
1238                label.classList.toggle('active', checkbox.checked);
1239                if (checkbox.checked) {
1240                    const color = label.dataset.tagColor;
1241                    label.style.backgroundColor = color;
1242                    label.style.borderColor = color;
1243                    label.style.color = getContrastTextColor(color);
1244                } else {
1245                    label.style.backgroundColor = '';
1246                    label.style.borderColor = '';
1247                    label.style.color = '';
1248                }
1249            }
1250            if (!checkbox.checked) {
1251                return;
1252            }
1253
1254            const id = String(checkbox.dataset.tagId);
1255            const group = checkbox.dataset.tagGroup || '';
1256            selectedIds.push(id);
1257            if (!selectedByGroup.has(group)) {
1258                selectedByGroup.set(group, []);
1259            }
1260            selectedByGroup.get(group).push(id);
1261        });
1262
1263        languageCheckboxes.forEach((checkbox) => {
1264            const label = checkbox.closest('.agenda-tag-filter-option');
1265            if (label) {
1266                label.classList.toggle('active', checkbox.checked);
1267                if (!checkbox.checked) {
1268                    label.style.backgroundColor = '';
1269                    label.style.borderColor = '';
1270                    label.style.color = '';
1271                }
1272            }
1273            if (checkbox.checked) {
1274                selectedLanguages.push(String(checkbox.dataset.agendaLanguage || '').trim());
1275            }
1276        });
1277
1278        typeCheckboxes.forEach((checkbox) => {
1279            const label = checkbox.closest('.agenda-tag-filter-option');
1280            if (label) {
1281                label.classList.toggle('active', checkbox.checked);
1282                if (!checkbox.checked) {
1283                    label.style.backgroundColor = '';
1284                    label.style.borderColor = '';
1285                    label.style.color = '';
1286                }
1287            }
1288            if (checkbox.checked) {
1289                const typeId = String(checkbox.dataset.agendaTypeId || '').trim();
1290                if (typeId) {
1291                    selectedTypeIds.push(typeId);
1292                }
1293            }
1294        });
1295
1296        const matchesSelectedTags = (item) => {
1297            const itemTagIds = new Set((item.dataset.agendaTagIds || '').split(',').filter(Boolean));
1298            return Array.from(selectedByGroup.values()).every((groupIds) =>
1299                groupIds.some((tagId) => itemTagIds.has(tagId))
1300            );
1301        };
1302
1303        const matchesSelectedLanguages = (item) => {
1304            if (selectedLanguages.length === 0) {
1305                return true;
1306            }
1307
1308            const itemLanguage = normalizeLanguage(item.dataset.agendaLanguage);
1309            return selectedLanguages.some((language) => normalizeLanguage(language) === itemLanguage);
1310        };
1311
1312        const matchesSelectedTypes = (item) => selectedTypeIds.length === 0
1313            || selectedTypeIds.includes(String(item.dataset.agendaTypeId || '').trim());
1314
1315        const matchesSelectedFilters = (item) => matchesSelectedTags(item)
1316            && matchesSelectedLanguages(item)
1317            && matchesSelectedTypes(item);
1318        const selectedCount = selectedIds.length + selectedLanguages.length + selectedTypeIds.length;
1319
1320        let visibleCount = 0;
1321        if (crossRoomEnabled) {
1322            if (selectedCount > 0) {
1323                const firstActivation = !resultsMode;
1324                if (firstActivation) {
1325                    savedAgendaHash = window.location.hash;
1326                }
1327
1328                ensurePlaceholders();
1329                restoreMovedItems();
1330                const matchingItems = [];
1331                items.forEach((item) => {
1332                    const visible = matchesSelectedFilters(item);
1333                    item.classList.toggle('d-none', !visible);
1334                    if (visible) {
1335                        visibleCount += 1;
1336                        matchingItems.push(item);
1337                    }
1338                });
1339
1340                setResultsMode(true);
1341                renderTagResults(matchingItems);
1342                updateResultSummary(matchingItems);
1343                setStatusVisibility(resultEmpty, matchingItems.length === 0);
1344                if (resultEmpty) {
1345                    resultEmpty.textContent = filter.dataset.emptyMessage || '';
1346                }
1347                setStatusVisibility(empty, false);
1348                if (firstActivation) {
1349                    try {
1350                        results.focus({ preventScroll: true });
1351                    } catch (error) {
1352                        results.focus();
1353                    }
1354                }
1355            } else {
1356                restoreMovedItems(true);
1357                items.forEach((item) => {
1358                    item.classList.remove('d-none');
1359                    setResultContextVisibility(item, false);
1360                });
1361                clearResults();
1362                setStatusVisibility(resultEmpty, false);
1363                setStatusVisibility(empty, false);
1364                setResultsMode(false);
1365                restoreAgendaState();
1366            }
1367        } else {
1368            items.forEach((item) => {
1369                const visible = matchesSelectedFilters(item);
1370                item.classList.toggle('d-none', !visible);
1371                if (visible) {
1372                    visibleCount += 1;
1373                }
1374            });
1375            setStatusVisibility(empty, selectedCount > 0 && visibleCount === 0);
1376            if (empty) {
1377                empty.textContent = filter.dataset.emptyMessage || '';
1378            }
1379        }
1380
1381        if (count) {
1382            count.textContent = String(selectedCount);
1383            setFilterControlVisibility(count, selectedCount > 0);
1384        }
1385        if (clear) {
1386            setFilterControlVisibility(clear, selectedCount > 0);
1387        }
1388
1389        if (updateAddress) {
1390            updateUrl(selectedIds, selectedLanguages, selectedTypeIds);
1391        }
1392    };
1393
1394    const applyUrlState = () => {
1395        const url = new URL(window.location.href);
1396        const rawIds = (url.searchParams.get('tags') || '')
1397            .split(',')
1398            .filter(Boolean);
1399        const rawLanguages = url.searchParams.getAll('languages').map((language) => language.trim()).filter(Boolean);
1400        const rawTypeIds = (url.searchParams.get('types') || '')
1401            .split(',')
1402            .filter(Boolean);
1403        const knownIds = new Set(tagCheckboxes.map((checkbox) => String(checkbox.dataset.tagId)));
1404        const validIds = rawIds.filter((id) => knownIds.has(id));
1405        const requested = new Set(validIds);
1406        tagCheckboxes.forEach((checkbox) => {
1407            checkbox.checked = requested.has(String(checkbox.dataset.tagId));
1408        });
1409        const knownLanguages = new Map(languageCheckboxes.map((checkbox) => [
1410            normalizeLanguage(checkbox.dataset.agendaLanguage),
1411            String(checkbox.dataset.agendaLanguage || '').trim(),
1412        ]));
1413        const validLanguages = [];
1414        rawLanguages.forEach((language) => {
1415            const canonical = knownLanguages.get(normalizeLanguage(language));
1416            if (canonical && !validLanguages.some((value) => normalizeLanguage(value) === normalizeLanguage(canonical))) {
1417                validLanguages.push(canonical);
1418            }
1419        });
1420        const requestedLanguages = new Set(validLanguages.map(normalizeLanguage));
1421        languageCheckboxes.forEach((checkbox) => {
1422            checkbox.checked = requestedLanguages.has(normalizeLanguage(checkbox.dataset.agendaLanguage));
1423        });
1424        const knownTypeIds = new Set(typeCheckboxes.map((checkbox) => String(checkbox.dataset.agendaTypeId)));
1425        const validTypeIds = rawTypeIds.filter((typeId) => knownTypeIds.has(typeId));
1426        const requestedTypeIds = new Set(validTypeIds);
1427        typeCheckboxes.forEach((checkbox) => {
1428            checkbox.checked = requestedTypeIds.has(String(checkbox.dataset.agendaTypeId));
1429        });
1430        if (
1431            rawIds.length !== validIds.length
1432            || rawIds.some((id, index) => id !== validIds[index])
1433            || rawLanguages.length !== validLanguages.length
1434            || rawLanguages.some((language, index) => language !== validLanguages[index])
1435            || rawTypeIds.length !== validTypeIds.length
1436            || rawTypeIds.some((typeId, index) => typeId !== validTypeIds[index])
1437        ) {
1438            updateUrl(validIds, validLanguages, validTypeIds, true);
1439        }
1440        apply(false);
1441    };
1442
1443    checkboxes.forEach((checkbox) => checkbox.addEventListener('change', () => apply(true)));
1444    if (clear) {
1445        clear.addEventListener('click', () => {
1446            checkboxes.forEach((checkbox) => {
1447                checkbox.checked = false;
1448            });
1449            apply(true);
1450            setFilterControlVisibility(count, false);
1451            setFilterControlVisibility(clear, false);
1452        });
1453    }
1454
1455    const knownTagIds = new Set(tagCheckboxes.map((checkbox) => String(checkbox.dataset.tagId)));
1456    const filterByTagId = (tagId) => {
1457        if (!knownTagIds.has(tagId)) {
1458            return;
1459        }
1460        tagCheckboxes.forEach((checkbox) => {
1461            checkbox.checked = String(checkbox.dataset.tagId) === tagId;
1462        });
1463        apply(true);
1464        filter.scrollIntoView({ behavior: 'smooth', block: 'start' });
1465    };
1466
1467    const knownTypeIds = new Set(typeCheckboxes.map((checkbox) => String(checkbox.dataset.agendaTypeId)));
1468    const filterByTypeId = (typeId) => {
1469        const normalizedTypeId = String(typeId || '').trim();
1470        if (!knownTypeIds.has(normalizedTypeId)) {
1471            return;
1472        }
1473        typeCheckboxes.forEach((checkbox) => {
1474            checkbox.checked = String(checkbox.dataset.agendaTypeId) === normalizedTypeId;
1475        });
1476        apply(true);
1477        filter.scrollIntoView({ behavior: 'smooth', block: 'start' });
1478    };
1479
1480    const filterByLanguage = (language) => {
1481        const normalizedLanguage = normalizeLanguage(language);
1482        if (!normalizedLanguage || !languageCheckboxes.some((checkbox) =>
1483            normalizeLanguage(checkbox.dataset.agendaLanguage) === normalizedLanguage
1484        )) {
1485            return;
1486        }
1487        languageCheckboxes.forEach((checkbox) => {
1488            checkbox.checked = normalizeLanguage(checkbox.dataset.agendaLanguage) === normalizedLanguage;
1489        });
1490        apply(true);
1491        filter.scrollIntoView({ behavior: 'smooth', block: 'start' });
1492    };
1493
1494    document.querySelectorAll('.agenda-tag[data-agenda-tag-id]').forEach((pill) => {
1495        const tagId = String(pill.dataset.agendaTagId);
1496        pill.addEventListener('click', () => filterByTagId(tagId));
1497        pill.addEventListener('keydown', (event) => {
1498            if (event.key === 'Enter' || event.key === ' ') {
1499                event.preventDefault();
1500                filterByTagId(tagId);
1501            }
1502        });
1503    });
1504
1505    document.querySelectorAll('[data-agenda-language-filter]').forEach((pill) => {
1506        const language = pill.dataset.agendaLanguageFilter || '';
1507        pill.addEventListener('click', () => filterByLanguage(language));
1508        pill.addEventListener('keydown', (event) => {
1509            if (event.key === 'Enter' || event.key === ' ') {
1510                event.preventDefault();
1511                filterByLanguage(language);
1512            }
1513        });
1514    });
1515
1516    document.querySelectorAll('[data-agenda-type-filter-id]').forEach((pill) => {
1517        const typeId = String(pill.dataset.agendaTypeFilterId || '');
1518        pill.addEventListener('click', () => filterByTypeId(typeId));
1519        pill.addEventListener('keydown', (event) => {
1520            if (event.key === 'Enter' || event.key === ' ') {
1521                event.preventDefault();
1522                filterByTypeId(typeId);
1523            }
1524        });
1525    });
1526
1527    window.addEventListener('popstate', applyUrlState);
1528    applyUrlState();
1529}
1530
1531if (document.readyState === 'loading') {
1532    document.addEventListener('DOMContentLoaded', initAgendaTagFilter);
1533} else {
1534    initAgendaTagFilter();
1535}

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.