1/** 2 * ========================================== 3 * general scripts for majority of pages 4 */ 5 6document.addEventListener("DOMContentLoaded", function () { 7 // main menu toggle 8 menu_toggle(); 9 10 // submenus toggle 11 subm_toggle(); 12 13 // submenus toggle 14 subm_toggle_mob(); 15 16 // sticky header 17 sticky_head(); 18 19 // init swiper sliders 20 init_sliders(); 21 22 // lazy load videos 23 init_lazy_videos(); 24 25 // init tabs 26 toggle_tabs(); 27 28 // init accordion 29 accordion(); 30 31 // detect hero-section page 32 check_hero(); 33 34 // flickr init (jQuery form tricks) 35 flickr_init(); 36 37 // custom form tweaks 38 cf7_tweaks(); 39}); 40 41/** 42 * main menu toggle 43 */ 44function menu_toggle() { 45 const body = document.querySelector("body"); 46 47 const toggle = document.querySelector(".menu-toggle"); 48 49 toggle.addEventListener("click", function () { 50 const isOpen = body.classList.toggle("menu-open"); 51 52 toggle.setAttribute("aria-expanded", isOpen); 53 54 // acivate the 1st menu item 55 const menuItems = document.querySelectorAll(".main-menu .menu-item"); 56 57 if (menuItems.length > 0) { 58 const bgImg = document.querySelector(".main-menu .bg-img"); 59 const firstItem = menuItems[0]; 60 61 const bg = menuItems[0]?.getAttribute("data-bg"); 62 bgImg.style.background = bg || "none"; 63 64 // ð Add the class that contains the hover styles 65 firstItem.classList.add("is-active"); 66 } 67 }); 68} 69 70/** 71 * submenus hover + bgImage 72 */ 73function subm_toggle2() { 74 /* const langClass = Array.from(document.body.classList).find((c) => 75 c.startsWith("language-") 76 ); 77 const lang = langClass ? langClass.slice(-3) : null; */ 78 79 const menu = document.querySelector('.main-menu ul[id^="menu-"]'); 80 81 const wrapper = document.getElementById("sub-wrapper"); 82 const bgImg = document.querySelector(".main-menu .bg-img"); 83 84 if (!menu || !wrapper || !bgImg) return; 85 86 // Delegate hover events to menu items 87 menu.querySelectorAll(".menu-item").forEach((menuItem) => { 88 const link = menuItem.querySelector("a"); 89 const submenu = menuItem.querySelector(".sub-menu"); 90 91 menuItem.addEventListener("mouseenter", () => { 92 const bg = menuItem?.getAttribute("data-bg"); 93 bgImg.style.background = bg || "none"; 94 95 // Clone submenu into wrapper 96 wrapper.innerHTML = ""; 97 if (submenu) { 98 const clone = submenu.cloneNode(true); 99 wrapper.append(clone); 100 101 menuItem.classList.add("submenu-open"); 102 } 103 }); 104 105 // When leaving the menu item 106 menuItem.addEventListener("mouseleave", (e) => { 107 // Only clear if mouse leaves the whole menu area 108 if (!menu.contains(e.relatedTarget)) { 109 bgImg.style.background = "none"; 110 wrapper.innerHTML = ""; 111 } 112 113 menuItem.classList.remove("submenu-open"); 114 }); 115 }); 116} 117 118/** 119 * submenus hover + bgImage 120 */ 121function subm_toggle() { 122 /* const langClass = Array.from(document.body.classList).find((c) => 123 c.startsWith("language-") 124 ); 125 const lang = langClass ? langClass.slice(-3) : null; */ 126 127 const menu = document.querySelector('.main-menu ul[id^="menu-"]'); 128 if (!menu) return; 129 130 const bgImg = document.querySelector(".main-menu .bg-img"); 131 const menuItems = document.querySelectorAll(".main-menu .menu-item"); 132 133 if (!menu || !bgImg) return; 134 135 // Delegate hover events to menu items 136 menuItems.forEach((menuItem) => { 137 const link = menuItem.querySelector("a"); 138 const submenu = menuItem.querySelector(".sub-menu"); 139 140 if (!submenu) return; 141 142 menuItem.addEventListener("mouseenter", () => { 143 // deactivate all submenu items 144 menuItems.forEach((i) => i.classList.remove("is-active")); 145 146 menuItem.classList.add("is-active"); 147 148 const mediaType = menuItem?.getAttribute("data-media-type"); 149 const videoUrl = menuItem?.getAttribute("data-video-url"); 150 const bg = menuItem?.getAttribute("data-bg"); 151 152 // Clear existing video 153 const existingVideo = bgImg.querySelector("video"); 154 if (existingVideo) { 155 existingVideo.remove(); 156 } 157 158 if (mediaType === "video" && videoUrl) { 159 // Hide background image 160 bgImg.style.background = "none"; 161 // Add video element 162 const video = document.createElement("video"); 163 video.autoplay = true; 164 video.muted = true; 165 video.loop = true;
166 video.playsInline = true; 167 video.className = "menu-bg-video"; 168 const source = document.createElement("source"); 169 source.src = videoUrl; 170 source.type = "video/mp4"; 171 video.appendChild(source); 172 bgImg.appendChild(video); 173 } else { 174 // Use background image 175 bgImg.style.background = bg || "none"; 176 } 177 178 menuItem.classList.add("submenu-open"); 179 }); 180 181 // When leaving the menu item 182 menuItem.addEventListener("mouseleave", (e) => { 183 // Only clear if mouse leaves the whole menu area 184 if (!menuItem.contains(e.relatedTarget)) { 185 menuItem.classList.remove("is-active"); 186 menuItem.classList.remove("submenu-open"); 187 188 if (!menu.contains(e.relatedTarget)) { 189 bgImg.style.background = "none"; 190 const existingVideo = bgImg.querySelector("video"); 191 if (existingVideo) { 192 existingVideo.remove(); 193 } 194 wrapper.innerHTML = ""; 195 } 196 } 197 }); 198 }); 199} 200 201/** 202 * submenus open on mob 203 */ 204function subm_toggle_mob() { 205 const menu = document.querySelector('.main-menu-mob ul[id^="menu-"]'); 206 207 if (!menu) return; 208 209 const menuItems = menu.querySelectorAll(".menu-item"); 210 211 // delegate click events on menu items 212 menuItems.forEach((menuItem) => { 213 const submenu = menuItem.querySelector(".sub-menu"); 214 215 if (!submenu) return; 216 217 menuItem.addEventListener("click", (e) => { 218 const link = e.target.closest("a"); 219 220 // If click is inside submenu (child link), allow navigation 221 if (link && link.closest(".sub-menu")) { 222 return; 223 } 224 225 e.preventDefault(); 226 227 menuItems.forEach((i) => i.classList.remove("is-active", "submenu-open")); 228 229 menuItem.classList.add("is-active", "submenu-open"); 230 }); 231 }); 232} 233 234/** 235 * make header sticky when scrolling down 236 */ 237function sticky_head() { 238 const header = document.getElementById("site-header"); 239 240 if (!header) { 241 console.error("Error: Element with ID 'site-header' not found."); 242 return; // Exit the function if the element is missing 243 } 244 245 const stickyOffset = header.offsetTop + 200; 246 247 window.onscroll = function () { 248 if (window.scrollY >= stickyOffset) { 249 header.classList.add("sticky"); 250 } else { 251 header.classList.remove("sticky"); 252 } 253 }; 254} 255 256/** 257 * init swiper sliders - per-position 258 */ 259function init_sliders() { 260 // hero 261 let args = { 262 loop: true, 263 slidesPerView: 1, 264 spaceBetween: 0, 265 266 speed: 700, 267 preloadImages: false, // prevent full preload 268 lazy: { 269 loadPrevNext: true, // optional: preload next/prev slides for smooth transition 270 }, 271 // Progress bar 272 pagination: { 273 el: ".swiper-pagination-progressbar", 274 type: "progressbar", 275 }, 276 autoplay: { 277 delay: 5000, 278 disableOnInteraction: false, // optional but recommended 279 }, 280 }; 281 282 set_slider("hero", args); 283 284 // gallery 285 args = { 286 loop: false, 287 centeredSlidesBounds: true, 288 centeredSlides: false, 289 watchSlidesProgress: true, 290 slidesPerView: "auto", 291 preloadImages: false, // prevent full preload 292 lazy: { 293 loadPrevNext: true, // optional: preload next/prev slides for smooth transition 294 }, 295 breakpoints: { 296 320: { 297 slidesPerView: 1, 298 }, 299 960: { 300 slidesPerView: 3, 301 }, 302 1440: { 303 slidesPerView: "auto", 304 }, 305 }, 306 }; 307 set_slider("gallery", args); 308 309 // packages 310 args = { 311 preloadImages: false, // prevent full preload 312 lazy: { 313 loadPrevNext: true, // optional: preload next/prev slides for smooth transition 314 }, 315 breakpoints: { 316 320: { 317 slidesPerView: 1, 318 }, 319 960: { 320 slidesPerView: 3, 321 }, 322 }, 323 }; 324 set_slider("packages", args); 325 326 // teaser2 327 args = { 328 centeredSlidesBounds: true, 329 centeredSlides: false, 330 watchSlidesProgress: true, 331 spaceBetween: 34, 332 preloadImages: false, // prevent full preload 333 lazy: { 334 loadPrevNext: true, // optional: preload next/prev slides for smooth transition 335 }, 336 breakpoints: { 337 320: { 338 spaceBetween: 34, 339 slidesPerView: "auto", 340 }, 341 960: { 342 spaceBetween: 50, 343 slidesPerView: 2, 344 }, 345 }, 346 }; 347 set_slider("teaser2", args); 348 349 // teaser3 350 args = { 351 enabled: true, 352 preloadImages: false, // prevent full preload 353 lazy: { 354 loadPrevNext: true, // optional: preload next/prev slides for smooth transition 355 }, 356 breakpoints: { 357 320: { 358 slidesPerView: 1, 359 }, 360 960: { 361 slidesPerView: 3, 362 }, 363 }, 364 }; 365 set_slider("teaser3", args); 366} 367 368/** 369 * Initialize lazy loading for videos 370 */ 371function init_lazy_videos() { 372 // Check if Intersection Observer is supported 373 if (!('IntersectionObserver' in window)) { 374 // Fallback: load all videos immediately 375 document.querySelectorAll('video[data-lazy-video="true"]').forEach((video) => { 376 video.setAttribute('preload', 'auto'); 377 }); 378 return; 379 } 380 381 // Find all videos with lazy loading enabled 382 const lazyVideos = document.querySelectorAll('video[data-lazy-video="true"]'); 383 384 if (lazyVideos.length === 0) return; 385 386 // Create Intersection Observer with rootMargin for earlier loading 387 const videoObserver = new IntersectionObserver( 388 (entries) => {
389 entries.forEach((entry) => { 390 if (entry.isIntersecting) { 391 const video = entry.target; 392 // Change preload to auto to start loading 393 video.setAttribute('preload', 'auto'); 394 // Load the video 395 video.load(); 396 // Stop observing this video 397 videoObserver.unobserve(video); 398 } 399 }); 400 }, 401 { 402 rootMargin: '50px', // Start loading 50px before video enters viewport 403 threshold: 0.01, 404 } 405 ); 406 407 // Observe all lazy videos 408 lazyVideos.forEach((video) => { 409 videoObserver.observe(video); 410 }); 411} 412 413/** 414 * Load videos in Swiper slider when slide becomes active 415 */ 416function load_swiper_videos(swiperInstance) { 417 if (!swiperInstance) return; 418 419 // Load video in current slide 420 const currentSlide = swiperInstance.slides[swiperInstance.activeIndex]; 421 if (currentSlide) { 422 const video = currentSlide.querySelector('video[data-lazy-video="true"]'); 423 if (video && video.getAttribute('preload') === 'none') { 424 video.setAttribute('preload', 'auto'); 425 video.load(); 426 } 427 } 428 429 // Preload videos in adjacent slides (next/prev) 430 const nextIndex = swiperInstance.activeIndex + 1; 431 const prevIndex = swiperInstance.activeIndex - 1; 432 433 [nextIndex, prevIndex].forEach((index) => { 434 if (swiperInstance.slides[index]) { 435 const video = swiperInstance.slides[index].querySelector('video[data-lazy-video="true"]'); 436 if (video && video.getAttribute('preload') === 'none') { 437 video.setAttribute('preload', 'metadata'); 438 video.load(); 439 } 440 } 441 }); 442} 443 444// set any slider 445function set_slider(pos, args = {}) { 446 const wrapper = `swiper-container-${pos}`; 447 448 document.querySelectorAll(`[class*="${wrapper}"]`).forEach((el) => { 449 // slider unique id 450 const id = el.dataset.id || ""; 451 452 const argset = get_special(el, structuredClone(args)); 453 454 const defaults = { 455 loop: true, 456 slidesPerView: 2, 457 spaceBetween: 50, 458 autoplay: false, 459 speed: 300, 460 navigation: { 461 nextEl: `.swiper-${pos}-next-${id}`, 462 prevEl: `.swiper-${pos}-prev-${id}`, 463 }, 464 }; 465 466 const options = { 467 ...defaults, 468 ...argset, 469 }; 470 471 const swiperInstance = new Swiper(el, options); 472 473 // Load videos when slide changes 474 swiperInstance.on('slideChange', () => { 475 load_swiper_videos(swiperInstance); 476 }); 477 478 // Load videos in initial slide 479 load_swiper_videos(swiperInstance); 480 }); 481} 482 483// get data attr for special settings 484function get_special(el, args) { 485 // disable on mobile 486 const disableOnMob = el.dataset.disable === "1"; // from ACF 487 488 args.breakpoints ||= {}; 489 490 if (disableOnMob) { 491 args.breakpoints[0] = { enabled: true }; 492 args.breakpoints[320] = { 493 ...args.breakpoints[320], 494 enabled: true, 495 }; 496 args.breakpoints[960] = { 497 ...args.breakpoints[960], 498 enabled: true, 499 }; 500 } 501 502 return args; 503} 504 505/** 506 * tabs (toggle tabs content) 507 */ 508function toggle_tabs() { 509 document.querySelectorAll(".wrapper.tabs").forEach((tabset) => { 510 const headers = tabset.querySelectorAll("[data-key].tabs-header"); 511 512 headers.forEach((header) => { 513 header.addEventListener("click", () => { 514 const key = header.dataset.key; 515 516 // get all headers and all content 517 const allHeaders = tabset.querySelectorAll(".tabs-header"); 518 const allContent = tabset.querySelectorAll(".tabs-content"); 519 520 // find the matching content 521 const content = tabset.querySelector( 522 `[data-key="${key}"].tabs-content` 523 ); 524 525 if (!content) return; 526 527 // deactivate all headers' buttons 528 allHeaders.forEach((head) => { 529 head.classList.remove("active"); 530 }); 531 532 allContent.forEach((cnt) => { 533 cnt.classList.remove("active"); 534 }); 535 536 header.classList.add("active"); 537 content.classList.add("active"); 538 }); 539 }); 540 }); 541} 542 543/** 544 * accordion (classic) 545 * - .accordion .accordion-item {.accordion-header .accordion-content} 546 */ 547function accordion() { 548 const accord = document.querySelector( 549 ".accordion > .accordion-item:first-child" 550 ); 551 552 if (!accord) return; 553 554 accord.classList.add("active"); 555 556 document.querySelectorAll(".accordion-item").forEach((item) => { 557 item.addEventListener("click", () => { 558 const isOpen = item.classList.contains("active"); 559 560 // Close all 561 document 562 .querySelectorAll(".accordion-item") 563 .forEach((c) => c.classList.remove("active"));
564 565 // Open clicked one 566 if (!isOpen) item.classList.add("active"); 567 }); 568 }); 569} 570 571/** 572 * detect hero-section page 573 */ 574function check_hero() { 575 const has_hero = document.querySelector(".sect-hero"); 576 577 if (!has_hero) return; 578 579 document.body.classList.add("has-hero"); 580} 581 582/** 583 * flickr init (jQuery form tricks) 584 */ 585function flickr_init() { 586 const el = document.getElementById("travel_period"); 587 if (!el) return; 588 589 flatpickr(el, { 590 mode: "range", 591 dateFormat: "d.m.Y", 592 minDate: "today", // optional: prevent past dates 593 }); 594} 595 596/** 597 * custom form tweaks 598 */ 599function cf7_tweaks() { 600 601 const repeater = document.querySelector('.wpcf7-form label[id^="repeater_"]'); 602 603 if (!repeater) { 604 // no conditional repeater field set on page 605 return; 606 } 607 608 const rep_id = repeater.id; 609 610 const rep_fields = document.querySelectorAll(`#${rep_id}_fields label`); 611 612 rep_fields.forEach((field, i) => { 613 let el = field.querySelector(`#kid_${i + 1}`); 614 615 // clear value 616 el.value = ""; 617 618 // remove required class and attributes 619 el.classList.remove("wpcf7-validates-as-required"); 620 el.removeAttribute("aria-required"); 621 622 // remove previous invalid state 623 el.classList.remove("wpcf7-not-valid"); 624 el.removeAttribute("aria-invalid"); 625 626 // remove tip span if exists 627 const tip = field.querySelector(".wpcf7-not-valid-tip"); 628 if (tip) tip.remove(); 629 630 /* // **reset CF7 internal validation** 631 if (typeof wpcf7 !== "undefined") { 632 wpcf7.clearResponse(el.closest(".wpcf7-form")); 633 } */ 634 }); 635 636 repeater.addEventListener("change", (e) => { 637 const slct = parseInt(e.target.value, 10); 638 639 rep_fields.forEach((field, i) => { 640 let el = field.querySelector(`#kid_${i + 1}`); 641 // show all repeating fields 642 field.classList.add("hideMe"); 643 el.value = ""; 644 el.classList.add("wpcf7-validates-as-required"); 645 el.setAttribute("aria-required", "true"); 646 if (i < slct) { 647 // hide after the selected number 648 field.classList.remove("hideMe"); 649 } else { 650 el.value = ""; 651 el.classList.remove("wpcf7-validates-as-required"); 652 el.removeAttribute("aria-required"); 653 el.classList.remove("wpcf7-not-valid"); 654 el.removeAttribute("aria-invalid"); 655 } 656 }); 657 }); 658} 659 660function cf7_tweaks2() { 661 const numKids = document.getElementById("number_kids"); 662 const kid1 = document.getElementById("lab_kid_1"); 663 const kid2 = document.getElementById("lab_kid_2"); 664 665 if (numKids > 0) { 666 numKids.addEventListener("change", () => { 667 const index = numKids.selectedIndex; 668 669 kid1.style.visibility = "hidden"; 670 kid2.style.visibility = "hidden"; 671 672 if (index === 1) { 673 kid1.style.visibility = "visible"; 674 } else if (index === 2) { 675 kid1.style.visibility = "visible"; 676 kid2.style.visibility = "visible"; 677 } 678 }); 679 } 680}
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.