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.