PageSourceSearch

https://www.ysb.se/wp-content/plugins/Menu/assets/js/frontend.js?ver=1.2.6

js ysb.se collected 2026-09-24 23:04:59 UTC 38,635 bytes, 924 lines download raw bytes

1
2
3(function($) {
4    'use strict';
5
6    class SlideMenuPro {
7        constructor() {
8            this.menu = $('#slide-menu-pro');
9            this.overlay = $('.slide-menu-overlay');
10            this.triggers = $('.slide-menu-trigger:not(.slide-menu-trigger-right)');
11            this.closeBtn = $('.slide-menu-close');
12            
13            // Right menu
14            this.menuRight = $('#slide-menu-pro-right');
15            this.overlayRight = $('.slide-menu-overlay-right');
16            this.triggersRight = $('.slide-menu-trigger-right');
17            this.closeBtnRight = $('.slide-menu-close-right');
18            
19            // Restaurant menu
20            this.menuRestaurant = $('#slide-menu-pro-restaurant');
21            this.overlayRestaurant = $('.slide-menu-overlay-restaurant');
22            this.closeBtnRestaurant = $('.slide-menu-close-restaurant');
23            this.backBtnRestaurant = $('.slide-menu-back-restaurant');
24            
25            this.dropdowns = null;
26            this.isOpen = false;
27            this.isRightOpen = false;
28            this.isRestaurantOpen = false;
29            this.focusableElements = null;
30            this.lastFocusedElement = null;
31            this.scrollbarWidth = 0;
32            
33            this.init();
34        }
35        
36        init() {
37            this.calculateScrollbarWidth();
38            this.bindEvents();
39            this.setupKeyboardNavigation();
40            this.applyMobilePresetStyles();
41            this.setupScrollIsolation();
42            
43            // Re-apply on resize
44            $(window).on('resize', () => {
45                this.applyMobilePresetStyles();
46                this.calculateScrollbarWidth();
47            });
48        }
49        
50        setupScrollIsolation() {
51            // Scroll isolation is primarily handled by:
52            // 1. CSS overscroll-behavior: contain on menu panels
53            // 2. Lenis stop/start when menus open/close
54            // This method adds stopPropagation for additional protection
55            
56            const menuPanels = [
57                '.slide-menu-pro',
58                '.slide-menu-pro-right',
59                '.slide-menu-pro-restaurant'
60            ];
61            
62            menuPanels.forEach(selector => {
63                // Use native event listener with passive: true to avoid warnings
64                // stopPropagation still works with passive listeners
65                document.addEventListener('wheel', (e) => {
66                    const menuElement = e.target.closest(selector);
67                    if (menuElement && menuElement.classList.contains('active')) {
68                        e.stopPropagation();
69                    }
70                }, { passive: true, capture: true });
71            });
72        }
73        
74        // Get Lenis instance - check common locations
75        getLenis() {
76            // Check for Lenis in common global locations
77            if (window.lenis) return window.lenis;
78            if (window.Lenis && window.Lenis.instance) return window.Lenis.instance;
79            if (window.smoothScroller) return window.smoothScroller;
80            
81            // Check for Lenis in Avada-specific location
82            if (window.avadaLenis) return window.avadaLenis;
83            
84            // Check for lenisInstance or similar naming
85            if (window.lenisInstance) return window.lenisInstance;
86            
87            return null;
88        }
89        
90        // Stop Lenis smooth scroll when menu opens
91        stopLenis() {
92            const lenis = this.getLenis();
93            if (lenis && typeof lenis.stop === 'function') {
94                lenis.stop();
95            }
96            
97            // Also add data attribute to html element (some implementations check this)
98            document.documentElement.setAttribute('data-lenis-prevent', '');
99        }
100        
101        // Start Lenis smooth scroll when all menus close
102        startLenis() {
103            // Only start if no menus are open
104            if (this.isOpen || this.isRightOpen || this.isRestaurantOpen) return;
105            
106            const lenis = this.getLenis();
107            if (lenis && typeof lenis.start === 'function') {
108                lenis.start();
109            }
110            
111            // Remove data attribute
112            document.documentElement.removeAttribute('data-lenis-prevent');
113        }
114        
115        calculateScrollbarWidth() {
116            // Calculate scrollbar width
117            const outer = document.createElement('div');
118            outer.style.visibility = 'hidden';
119            outer.style.overflow = 'scroll';
120            document.body.appendChild(outer);
121            
122            const inner = document.createElement('div');
123            outer.appendChild(inner);
124            
125            this.scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
126            
127            outer.parentNode.removeChild(outer);
128            
129            // Set CSS variable
130            document.documentElement.style.setProperty('--scrollbar-width', this.scrollbarWidth + 'px');
131        }
132        
133        applyMobilePresetStyles() {
134            const isMobile = window.innerWidth <= 768;
135            
136            // Apply mobile styles to left trigger buttons with presets
137            $('.slide-menu-trigger.preset-hjortviken:not(.slide-menu-trigger-right)').each(function() {
138                const $button = $(this);
139                const mobileMode = $button.data('mobile-mode') || 'icon';
140                const mobileIcon = $button.data('mobile-icon') || 'bars';
141                const mobileText = $button.data('mobile-text') || 'Meny';
142                const mobileSize = $button.data('mobile-size') || 60;
143                
144                const icons = {
145                    'bars': '=',
146                    'bars-rounded': '≡',
147                    'dots-3': '⋮',
148                    'dots-3h': '⋯',
149                    'grid': '⊞'
150                };
151                
152                const $iconSpan = $button.find('.slide-menu-trigger-icon, .slide-menu-trigger-left-icon');
153                const $textSpan = $button.find('.slide-menu-trigger-text, .slide-menu-trigger-left-text');
154                
155                if (isMobile) {
156                    // Apply mobile size
157                    $button.css({
158                        'width': mobileSize + 'px',
159                        'height': mobileSize + 'px'
160                    });
161                    
162                    if (mobileMode === 'icon') {
163                        // Show icon, hide text
164                        $iconSpan.text(icons[mobileIcon] || '=').show();
165                        $textSpan.hide();
166                    } else {
167                        // Show text, hide icon
168                        $iconSpan.hide();
169                        $textSpan.text(mobileText).show();
170                    }
171                } else {
172                    // Reset desktop styles - let CSS handle it
173                    $button.css({
174                        'width': '',
175                        'height': ''
176                    });
177                    
178                    // Restore both based on display mode from settings
179                    $iconSpan.show();
180                    $textSpan.show();
181                }
182            });
183            
184            // Apply mobile styles to right trigger buttons with presets
185            $('.slide-menu-trigger-right.preset-hjortviken').each(function() {
186                const $button = $(this);
187                const mobileMode = $button.data('mobile-mode') || 'text';
188                const mobileIcon = $button.data('mobile-icon') || 'bars';
189                const mobileText = $button.data('mobile-text') || 'Boka';
190                const mobileSize = $button.data('mobile-size') || 60;
191                
192                const icons = {
193                    'bars': '=',
194                    'bars-rounded': '≡',
195                    'dots-3': '⋮',
196                    'dots-3h': '⋯',
197                    'grid': '⊞'
198                };
199                
200                const $iconSpan = $button.find('.slide-menu-trigger-icon, .slide-menu-trigger-right-icon');
201                const $textSpan = $button.find('.slide-menu-trigger-text, .slide-menu-trigger-right-text');
202                
203                if (isMobile) {
204                    // Apply mobile size
205                    $button.css({
206                        'width': mobileSize + 'px',
207                        'height': mobileSize + 'px'
208                    });
209                    
210                    if (mobileMode === 'icon') {
211                        // Show icon, hide text
212                        $iconSpan.text(icons[mobileIcon] || '=').show();
213                        $textSpan.hide();
214                    } else {
215                        // Show text, hide icon
216                        $iconSpan.hide();
217                        $textSpan.text(mobileText).show();
218                    }
219                } else {
220                    // Reset desktop styles - let CSS handle it
221                    $button.css({
222                        'width': '',
223                        'height': ''
224                    });
225                    
226                    // Restore both based on display mode from settings
227                    $iconSpan.show();
228                    $textSpan.show();
229                }
230            });
231        }
232        
233        bindEvents() {
234            // Left menu triggers - handle dynamic language-based menus
235            this.triggers.on('click', (e) => {
236                e.preventDefault();
237                const $trigger = $(e.currentTarget);
238                const menuId = $trigger.attr('aria-controls');
239                this.menu = $('#' + menuId);
240                this.closeBtn = this.menu.find('.slide-menu-close');
241                this.overlay = $('.slide-menu-overlay');
242                
243                // Force reinitialization of dropdowns for this specific menu
244                this.dropdowns = null;
245                
246                // Blur the trigger to prevent stuck hover state
247                $trigger.blur();
248                
249                this.toggle();
250            });
251            
252            // Left menu close button - use delegation
253            $(document).on('click', '.slide-menu-close', (e) => {
254                e.preventDefault();
255                this.close();
256            });
257            
258            // Left menu overlay click
259            $('.slide-menu-overlay').on('click', () => {
260                this.close();
261            });
262            
263            // Right menu triggers - use delegation for dynamic language-based menus
264            $(document).on('click', '.slide-menu-trigger-right', (e) => {
265                e.preventDefault();
266                const $trigger = $(e.currentTarget);
267                const menuId = $trigger.attr('aria-controls');
268                this.menuRight = $('#' + menuId);
269                this.closeBtnRight = this.menuRight.find('.slide-menu-close-right');
270                this.overlayRight = $('.slide-menu-overlay-right');
271                
272                // Blur the trigger to prevent stuck hover state
273                $trigger.blur();
274                
275                this.toggleRight();
276            });
277            
278            // Right menu close button - use delegation
279            $(document).on('click', '.slide-menu-close-right', (e) => {
280                e.preventDefault();
281                this.closeRight();
282            });
283            
284            // Right menu overlay click
285            $('.slide-menu-overlay-right').on('click', () => {
286                this.closeRight();
287            });
288            
289            // Direct restaurant open — set CSS ID on any Avada button/element:
290            //   open-restaurant       → default language
291            //   open-restaurant-sv    → Swedish
292            //   open-restaurant-en    → English
293            //   open-restaurant-dk    → Danish
294            //   open-restaurant-no    → Norwegian
295            // Also matches clicks on child elements inside the ID'd container
296            $(document).on('click', '#open-restaurant, #open-restaurant-sv, #open-restaurant-en, #open-restaurant-dk, #open-restaurant-no, #open-restaurant a, #open-restaurant-sv a, #open-restaurant-en a, #open-restaurant-dk a, #open-restaurant-no a', (e) => {
297                e.preventDefault();
298                e.stopPropagation();
299                const $el = $(e.currentTarget).closest('[id^="open-restaurant"]');
300                const id = $el.attr('id');
301                const lang = id === 'open-restaurant' ? '' : id.replace('open-restaurant-', '');
302                this.openRestaurant(lang);
303            });
304
305            // Restaurant menu close button
306            this.closeBtnRestaurant.on('click', (e) => {
307                e.preventDefault();
308                this.closeRestaurant();
309            });
310            
311            // Restaurant menu back button
312            this.backBtnRestaurant.on('click', (e) => {
313                e.preventDefault();
314                this.backToRightMenu();
315            });
316            
317            // Restaurant menu overlay click
318            this.overlayRestaurant.on('click', () => {
319                this.closeRestaurant();
320            });
321            
322            this.setupRestaurantTriggers();
323            
324            $(document).on('keydown', (e) => {
325                if (e.key === 'Escape') {
326                    if (this.isRestaurantOpen) {
327                        this.closeRestaurant();
328                    } else if (this.isOpen) {
329                        this.close();
330                    } else if (this.isRightOpen) {
331                        this.closeRight();
332                    }
333                }
334            });
335            
336        }
337        
338        setupKeyboardNavigation() {
339            this.menu.on('keydown', (e) => {
340                if (!this.isOpen) return;
341                
342                if (e.key === 'Tab') {
343                    this.handleTabKey(e);
344                }
345            });
346        }
347        
348        setupDropdowns() {
349            // Use event delegation on the menu container for reliability
350            // Remove any previous delegated handler
351            this.menu.off('click.dropdown', '.slide-menu-item > a[aria-haspopup="true"]');
352
353            // Set up delegated event handler (click only — touchstart causes false triggers on mobile scroll/drag)
354            this.menu.on('click.dropdown', '.slide-menu-item > a[aria-haspopup="true"]', (e) => {
355                const $link = $(e.currentTarget);
356                const $submenu = $link.next('.slide-menu-submenu');
357                const isExpanded = $link.attr('aria-expanded') === 'true';
358                const isMobile = window.innerWidth <= 768;
359                
360                if ($(e.target).hasClass('slide-menu-dropdown-icon')) {
361                    e.preventDefault();
362                    e.stopPropagation();
363                    
364                    // Close other dropdowns in this menu
365                    this.menu.find('.slide-menu-item > a[aria-haspopup="true"]').not($link).attr('aria-expanded', 'false');
366                    
367                    // Toggle this dropdown (CSS handles visibility based on aria-expanded)
368                    $link.attr('aria-expanded', !isExpanded);
369                    
370                    return;
371                }
372            });
373            
374            // Keyboard navigation
375            this.menu.off('keydown.dropdown', '.slide-menu-item > a[aria-haspopup="true"]');
376            this.menu.on('keydown.dropdown', '.slide-menu-item > a[aria-haspopup="true"]', (e) => {
377                const $link = $(e.currentTarget);
378                
379                if (e.key === 'Enter' || e.key === ' ') {
380                    e.preventDefault();
381                    $link.click();
382                }
383                
384                if (e.key === 'ArrowDown') {
385                    e.preventDefault();
386                    const $submenu = $link.next('.slide-menu-submenu');
387                    if ($link.attr('aria-expanded') === 'false') {
388                        $link.click();
389                    }
390                    setTimeout(() => {
391                        $submenu.find('a').first().focus();
392                    }, 100);
393                }
394            });
395        }
396        
397        toggle() {
398            if (this.isOpen) {
399                this.close();
400            } else {
401                this.open();
402            }
403        }
404        
405        toggleRight() {
406            if (this.isRightOpen) {
407                this.closeRight();
408            } else {
409                this.openRight();
410            }
411        }
412        
413        open() {
414            this.lastFocusedElement = document.activeElement;
415            
416            // Stop Lenis smooth scroll
417            this.stopLenis();
418            
419            // First, ensure any focus inside hidden menus is cleared
420            if (document.activeElement) {
421                const $activeEl = $(document.activeElement);
422                if ($activeEl.closest('[aria-hidden="true"]').length) {
423                    document.activeElement.blur();
424                }
425            }
426            
427            // Remove aria-hidden BEFORE adding active class and setting focus
428            this.menu.removeAttr('aria-hidden');
429            this.menu.addClass('active');
430            this.overlay.addClass('active');
431            
432            this.triggers.attr('aria-expanded', 'true');
433            
434            // Add class to body for scrollbar compensation
435            $('body').addClass('slide-menu-open').css('overflow', 'hidden');
436            
437            // Reset all dropdown aria states
438            this.menu.find('.slide-menu-item > a[aria-haspopup="true"]').attr('aria-expanded', 'false');
439            
440            // Setup event handlers using delegation (persists across open/close)
441            this.setupDropdowns();
442            
443            // Set focus after DOM has updated
444            setTimeout(() => {
445                if (this.closeBtn.length && this.menu.hasClass('active')) {
446                    this.closeBtn.get(0).focus();
447                    this.closeBtn.removeClass('focus-hover');
448                }
449                this.updateFocusableElements();
450            }, 100);
451            
452            this.isOpen = true;
453            
454            this.announceToScreenReader('Menu opened');
455        }
456        
457        close() {
458            // Blur any focused element inside the menu before hiding
459            if (document.activeElement && this.menu.has(document.activeElement).length) {
460                document.activeElement.blur();
461            }
462            
463            this.menu.removeClass('active');
464            this.overlay.removeClass('active');
465            
466            this.triggers.attr('aria-expanded', 'false');
467            
468            // Remove class from body to restore scrollbar
469            $('body').removeClass('slide-menu-open').css('overflow', '');
470            
471            if (this.dropdowns) {
472                this.dropdowns.attr('aria-expanded', 'false');
473                $('.slide-menu-submenu').slideUp(300);
474            }
475            
476            // Set aria-hidden after blur and before restoring focus elsewhere
477            this.menu.attr('aria-hidden', 'true');
478            
479            if (this.lastFocusedElement) {
480                setTimeout(() => {
481                    this.lastFocusedElement.focus();
482                }, 100);
483            }
484            
485            this.isOpen = false;
486            
487            // Resume Lenis smooth scroll
488            this.startLenis();
489            
490            this.announceToScreenReader('Menu closed');
491        }
492        
493        openRight() {
494            this.lastFocusedElement = document.activeElement;
495            
496            // Stop Lenis smooth scroll
497            this.stopLenis();
498            
499            // First, ensure any focus inside hidden menus is cleared
500            if (document.activeElement) {
501                const $activeEl = $(document.activeElement);
502                if ($activeEl.closest('[aria-hidden="true"]').length) {
503                    document.activeElement.blur();
504                }
505            }
506            
507            // Remove aria-hidden BEFORE adding active class and setting focus
508            this.menuRight.removeAttr('aria-hidden');
509            this.menuRight.addClass('active');
510            this.overlayRight.addClass('active');
511            
512            this.triggersRight.attr('aria-expanded', 'true');
513            
514            // Add class to body for scrollbar compensation
515            $('body').addClass('slide-menu-open').css('overflow', 'hidden');
516            
517            // Set focus after DOM has updated
518            setTimeout(() => {
519                if (this.closeBtnRight.length && this.menuRight.hasClass('active')) {
520                    this.closeBtnRight.get(0).focus();
521                }
522            }, 100);
523            
524            this.isRightOpen = true;
525            
526            this.announceToScreenReader('Right menu opened');
527        }
528        
529        closeRight() {
530            // Blur any focused element inside the menu before hiding
531            if (document.activeElement && this.menuRight.has(document.activeElement).length) {
532                document.activeElement.blur();
533            }
534            
535            this.menuRight.removeClass('active');
536            this.overlayRight.removeClass('active');
537            
538            this.triggersRight.attr('aria-expanded', 'false');
539            
540            // Remove class from body to restore scrollbar
541            $('body').removeClass('slide-menu-open').css('overflow', '');
542            
543            // Set aria-hidden after blur and before restoring focus elsewhere
544            this.menuRight.attr('aria-hidden', 'true');
545            
546            if (this.lastFocusedElement) {
547                setTimeout(() => {
548                    this.lastFocusedElement.focus();
549                }, 100);
550            }
551            
552            this.isRightOpen = false;
553            
554            // Resume Lenis smooth scroll
555            this.startLenis();
556            
557            this.announceToScreenReader('Right menu closed');
558        }
559        
560        setupRestaurantTriggers() {
561            // Use event delegation on document for right menu links
562            $(document).on('click', '.slide-menu-pro-right a', (e) => {
563                const $link = $(e.currentTarget);
564                
565                // Only proceed if this link has the restaurant trigger enabled
566                if (!$link.hasClass('restaurant-trigger') || $link.attr('data-restaurant-trigger') !== 'true') {
567                    // This is a normal link, let it work normally
568                    return;
569                }
570                
571                // This is a restaurant trigger link
572                e.preventDefault();
573                e.stopPropagation();
574                
575                // Remove focus from the clicked element to prevent aria-hidden warning
576                if (document.activeElement) {
577                    document.activeElement.blur();
578                }
579                
580                // Get the language from the active right menu
581                const $activeRightMenu = $('.slide-menu-pro-right.active');
582                const menuId = $activeRightMenu.attr('id') || '';
583                let currentLang = '';
584
585                // Extract language from menu ID (e.g., slide-menu-pro-right_sv -> sv)
586                if (menuId.includes('_')) {
587                    currentLang = menuId.split('_').pop();
588                }
589
590                this.openRestaurant(currentLang);
591            });
592        }
593        
594        openRestaurant(language = '') {
595            this.currentRestaurantLang = language;
596            this.cameFromRightMenu = this.isRightOpen;
597
598            // Update back label based on language
599            const $backLabel = $('.slide-menu-back-label');
600            if ($backLabel.length) {
601                const lang = language || 'sv';
602                const label = $backLabel.data('label-' + lang) || $backLabel.data('label-sv') || 'Boka';
603                $backLabel.text(label);
604            }
605            
606            // Stop Lenis smooth scroll
607            this.stopLenis();
608            
609            // First, ensure any focus inside hidden menus is cleared
610            if (document.activeElement) {
611                const $activeEl = $(document.activeElement);
612                if ($activeEl.closest('[aria-hidden="true"]').length) {
613                    document.activeElement.blur();
614                }
615            }
616            
617            if (this.isRightOpen) {
618                // Blur focus in right menu before hiding it
619                if (document.activeElement && this.menuRight.has(document.activeElement).length) {
620                    document.activeElement.blur();
621                }
622                
623                this.menuRight.removeClass('active');
624                this.menuRight.attr('aria-hidden', 'true');
625                this.overlayRight.removeClass('active');
626                this.isRightOpen = false;
627                
628                setTimeout(() => {
629                    this.menuRestaurant.removeAttr('aria-hidden');
630                    this.menuRestaurant.addClass('active');
631                    this.overlayRestaurant.addClass('active');
632                    
633                    setTimeout(() => {
634                        if (this.backBtnRestaurant.length && this.menuRestaurant.hasClass('active')) {
635                            this.backBtnRestaurant.get(0).focus();
636                        }
637                    }, 100);
638                }, 150);
639            } else {
640                this.menuRestaurant.removeAttr('aria-hidden');
641                this.menuRestaurant.addClass('active');
642                this.overlayRestaurant.addClass('active');
643                
644                setTimeout(() => {
645                    if (this.backBtnRestaurant.length && this.menuRestaurant.hasClass('active')) {
646                        this.backBtnRestaurant.get(0).focus();
647                    }
648                }, 100);
649            }
650            
651            // Add class to body for scrollbar compensation
652            $('body').addClass('slide-menu-open').css('overflow', 'hidden');
653            this.isRestaurantOpen = true;
654            
655            this.loadRestaurantTabs(language);
656            this.initializeBookingWidgets();
657            
658            this.announceToScreenReader('Restaurant menu opened');
659        }
660        
661        loadRestaurantTabs(language = '') {
662            const currentLang = language || this.currentRestaurantLang || '';
663            const $tabsContainer = $('.restaurant-menu-tabs');
664
665            // Store current language in tabs container
666            $tabsContainer.attr('data-current-lang', currentLang);
667
668            // Show loading state
669            $tabsContainer.html('<div class="tabs-loading">Loading...</div>');
670
671            // Load tags via AJAX
672            $.ajax({
673                url: slideMenuProAjax.ajaxUrl,
674                type: 'POST',
675                data: {
676                    action: 'slide_menu_get_restaurant_tabs',
677                    language_code: currentLang
678                },
679                success: (response) => {
680                    if (response.success && response.data.tabs && response.data.tabs.length > 0) {
681                        let tabsHtml = '';
682                        let firstTab = true;
683                        
684                        response.data.tabs.forEach(tab => {
685                            const activeClass = firstTab ? ' active' : '';
686                            tabsHtml += `<button class="restaurant-tab${activeClass}" data-tab="${tab.tag_slug}">${tab.tag_name}</button>`;
687                            firstTab = false;
688                        });
689                        
690                        $tabsContainer.html(tabsHtml);
691                        this.setupRestaurantTabs(currentLang);
692                    } else {
693                        $tabsContainer.html('<p style="color: #666; padding: 20px; text-align: center;">No categories available for this language</p>');
694                        // Still setup the tab system for filtering
695                        this.setupRestaurantTabs(currentLang);
696                    }
697                },
698                error: (xhr, status, error) => {
699                    $tabsContainer.html('<p style="color: #d63638; padding: 20px; text-align: center;">Error loading categories</p>');
700                }
701            });
702        }
703        
704        setupRestaurantTabs(language = '') {
705            const currentLang = language || this.currentRestaurantLang || '';
706            const currentLangNorm = String(currentLang || '').trim().toLowerCase();
707
708            // Get all restaurant items as an array to maintain order
709            const $allItems = $('.restaurant-item');
710
711            // First, filter restaurants by language (case-insensitive) and maintain DOM order
712            $allItems.each(function() {
713                const $item = $(this);
714                const itemLang = $item.data('language');
715                const itemLangNorm = (itemLang === undefined || itemLang === null) ? '' : String(itemLang).trim().toLowerCase();
716
717                // Language matching (case-insensitive: "DK" === "dk")
718                const matched = (itemLangNorm === currentLangNorm);
719
720                if (matched) {
721                    $item.attr('data-lang-match', 'true').css({'opacity': '1', 'transform': 'translateX(0)'}).show();
722                } else {
723                    $item.attr('data-lang-match', 'false').css({'opacity': '0', 'transform': 'translateX(20px)'}).hide();
724                }
725            });
726
727            // Setup tab click handlers
728            $('.restaurant-tab').off('click').on('click', function() {
729                const tab = $(this).data('tab');
730                const isFirstTab = $(this).index() === 0;
731                
732                $('.restaurant-tab').removeClass('active');
733                $(this).addClass('active');
734                
735                // Hide all restaurants first
736                $('.restaurant-item').hide();
737                
738                // Check if it's a "show all" tab (first tab or common "all" slugs)
739                const showAllSlugs = ['alla', 'all', 'tous', 'alle', 'todos'];
740                const isShowAll = isFirstTab || showAllSlugs.includes(tab.toLowerCase());
741                
742                if (isShowAll) {
743                    // Show all language-matched restaurants in their original DOM order
744                    $('.restaurant-item[data-lang-match="true"]').each(function() {
745                        $(this).css({'opacity': '1', 'transform': 'translateX(0)'}).show();
746                    });
747                } else {
748                    // Filter by tag among language-matched restaurants, maintaining DOM order
749                    $('.restaurant-item[data-lang-match="true"]').each(function() {
750                        const $item = $(this);
751                        const tags = $item.data('tags');
752                        
753                        if (!tags) {
754                            return; // Keep hidden
755                        }
756                        
757                        const tagList = tags.toString().split(',').map(t => t.trim());
758                        
759                        // Show if the item has the selected tag (maintaining DOM order)
760                        if (tagList.includes(tab)) {
761                            $item.css({'opacity': '1', 'transform': 'translateX(0)'}).show();
762                        }
763                    });
764                }
765            });
766            
767            // Trigger the first tab to ensure proper display
768            setTimeout(() => {
769                $('.restaurant-tab.active').trigger('click');
770            }, 100);
771        }
772        
773        closeRestaurant() {
774            // Blur any focused element inside the menu before hiding
775            if (document.activeElement && this.menuRestaurant.has(document.activeElement).length) {
776                document.activeElement.blur();
777            }
778            
779            this.menuRestaurant.removeClass('active');
780            this.overlayRestaurant.removeClass('active');
781            
782            // Remove class from body to restore scrollbar
783            $('body').removeClass('slide-menu-open').css('overflow', '');
784            
785            // Set aria-hidden after blur and before restoring focus elsewhere
786            this.menuRestaurant.attr('aria-hidden', 'true');
787            
788            if (this.lastFocusedElement) {
789                setTimeout(() => {
790                    this.lastFocusedElement.focus();
791                }, 100);
792            }
793            
794            this.isRestaurantOpen = false;
795            
796            // Resume Lenis smooth scroll
797            this.startLenis();
798            
799            this.announceToScreenReader('Restaurant menu closed');
800        }
801        
802        backToRightMenu() {
803            // Blur any focused element inside the restaurant menu before hiding
804            if (document.activeElement && this.menuRestaurant.has(document.activeElement).length) {
805                document.activeElement.blur();
806            }
807            
808            this.menuRestaurant.removeClass('active');
809            this.menuRestaurant.attr('aria-hidden', 'true');
810            this.overlayRestaurant.removeClass('active');
811            this.isRestaurantOpen = false;
812            
813            // Keep body class since we're staying in a menu
814            setTimeout(() => {
815                this.menuRight.removeAttr('aria-hidden');
816                this.menuRight.addClass('active');
817                this.overlayRight.addClass('active');
818                this.isRightOpen = true;
819                
820                setTimeout(() => {
821                    if (this.closeBtnRight.length && this.menuRight.hasClass('active')) {
822                        this.closeBtnRight.get(0).focus();
823                    }
824                }, 100);
825                
826                this.announceToScreenReader('Back to booking menu');
827            }, 300);
828        }
829        
830        updateFocusableElements() {
831            this.focusableElements = this.menu.find(
832                'a, button, input, select, textarea, [tabindex]:not
832([tabindex="-1"])'
833            ).filter(':visible');
834        }
835        
836        handleTabKey(e) {
837            this.updateFocusableElements();
838            
839            const firstElement = this.focusableElements.first()[0];
840            const lastElement = this.focusableElements.last()[0];
841            
842            if (e.shiftKey) {
843                // Shift + Tab
844                if (document.activeElement === firstElement) {
845                    e.preventDefault();
846                    lastElement.focus();
847                }
848            } else {
849                // Tab
850                if (document.activeElement === lastElement) {
851                    e.preventDefault();
852                    firstElement.focus();
853                }
854            }
855        }
856        
857        announceToScreenReader(message) {
858            const announcement = $('<div>')
859                .attr({
860                    'role': 'status',
861                    'aria-live': 'polite',
862                    'aria-atomic': 'true'
863                })
864                .addClass('sr-only')
865                .text(message);
866            
867            $('body').append(announcement);
868            
869            setTimeout(() => {
870                announcement.remove();
871            }, 1000);
872        }
873        
874        initializeBookingWidgets() {
875            // Initialize Bookvisit widgets after restaurant menu opens
876            // The widget code should already be in the DOM within .restaurant-widget-container
877            // We just need to ensure any scripts in the widget code are executed
878            
879            $('.restaurant-widget-container script').each(function() {
880                const scriptContent = $(this).html();
881                const scriptSrc = $(this).attr('src');
882                
883                if (scriptSrc) {
884                    // External script - load it
885                    const newScript = document.createElement('script');
886                    newScript.src = scriptSrc;
887                    document.body.appendChild(newScript);
888                } else if (scriptContent) {
889                    // Inline script - execute it
890                    try {
891                        eval(scriptContent);
892                    } catch (e) {
893                        console.error('Error executing widget script:', e);
894                    }
895                }
896            });
897        }
898    }
899    
900    $(document).ready(function() {
901        new SlideMenuPro();
902        
903        $('.slide-menu-pro:not(.slide-menu-pro-right) a[href^="#"]').on('click', function(e) {
904            const target = $(this.getAttribute('href'));
905            if (target.length) {
906                e.preventDefault();
907                
908                $('.slide-menu-pro').removeClass('active').attr('aria-hidden', 'true');
909                $('.slide-menu-overlay').removeClass('active');
910                $('.slide-menu-trigger').attr('aria-expanded', 'false');
911                $('body').css('overflow', '');
912                
913                setTimeout(() => {
914                    $('html, body').animate({
915                        scrollTop: target.offset().top - 100
916                    }, 600);
917                    
918                    target.attr('tabindex', '-1').focus();
919                }, 300);
920            }
921        });
922    });
923    
924})(jQuery);

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.