1/** 2 * WP-ImmoMakler gallery loader. 3 * 4 * Scans the DOM for elements marked with `data-immomakler-gallery`, reads 5 * their `data-config` JSON attribute, and instantiates one or two Swiper 6 * instances per element (main + optional thumbs). Idempotent â already 7 * initialised elements are skipped on rescan. 8 * 9 * Config shape consumed via data-config: 10 * 11 * { 12 * "main": { "selector": ".immomakler-gallery__main", "config": { ...swiper opts... } }, 13 * "thumbs"?: { "selector": ".immomakler-gallery__thumbs", "config": { ...swiper opts... } }, 14 * "lightgallery"?: { "dynamic": true, "dynamicEl": [...], "plugins": [...], ... } 15 * } 16 * 17 * When `thumbs` is present, the loader instantiates the thumbs swiper first 18 * and patches `main.config.thumbs.swiper` to reference it before instantiating 19 * the main swiper. 20 * 21 * When `lightgallery` is present, the loader instantiates lightGallery with 22 * the supplied options and wires a click handler on the main swiper's slides 23 * that opens the lightbox at the active slide index. Plugin-name strings in 24 * `lightgallery.plugins` (`"lgZoom"`, `"lgThumbnail"`, `"lgFullscreen"`, â¦) 25 * are mapped to the matching globals at runtime so the JSON payload stays 26 * pure data. 27 * 28 * Public API for AJAX-injected content: 29 * 30 * window.immomakler.gallery.init( root ) // re-scan a subtree 31 * document.dispatchEvent( 32 * new CustomEvent( 'immomakler-gallery:rescan', { detail: { root } } ) 33 * ); 34 * 35 * Each successfully initialised gallery dispatches an 36 * `immomakler-gallery:ready` event on its outer element with 37 * `detail.swiper` set to the main Swiper instance and `detail.lightgallery` 38 * (when applicable) set to the lightGallery instance. 39 *
40 * Vanilla JS by design â no jQuery dependency. See 41 * project_typescript_migration_plan.md for why this file stays untyped. 42 */ 43( function () { 44 'use strict'; 45 46 const SELECTOR = '[data-immomakler-gallery]'; 47 48 // Ceiling on slides a looping gallery may be grown to. Only a very short 49 // gallery of very narrow slides in a very wide container can approach it, 50 // and stopping there keeps a pathological config from filling the DOM â 51 // at the cost of the gap this guard otherwise closes. 52 const MAX_LOOP_SLIDES = 60; 53 const initialised = typeof WeakSet === 'function' ? new WeakSet() : null; 54 55 function isInitialised( el ) { 56 return initialised 57 ? initialised.has( el ) 58 : el.dataset.immomaklerGalleryReady === '1'; 59 } 60 61 function markInitialised( el ) { 62 if ( initialised ) { 63 initialised.add( el ); 64 } 65 el.dataset.immomaklerGalleryReady = '1'; 66 } 67 68 /** 69 * Resolve the Swiper constructor to use. 70 * 71 * Prefer `window.immomakler.Swiper` â our own pinned bundle, stashed 72 * there by the inline isolation script in HookInitializer so it survives 73 * a shared `window.Swiper` being overwritten by another plugin's Swiper 74 * (Elementor Pro ships its own). Fall back to the bare `window.Swiper` 75 * global so the loader keeps working when the isolation script isn't on 76 * the page (e.g. a host that enqueues the bundle through a different 77 * path, or the classic skin on an older build). 78 * 79 * @return {Function|undefined} The Swiper constructor, or undefined when 80 * no Swiper has loaded yet. 81 */ 82 function getSwiper() { 83 if ( 84 window.immomakler && 85 typeof window.immomakler.Swiper === 'function' 86 ) { 87 return window.immomakler.Swiper; 88 } 89 return window.Swiper; 90 } 91 92 /** 93 * Read the gallery config from the first child 94 * `<script type="application/json" class="immomakler-gallery__config">`. 95 * 96 * Falls back to the legacy `data-config` attribute when no script tag 97 * is present, so the loader stays compatible with any caller that 98 * hasn't been updated yet. The script-tag path is the canonical one â 99 * see the rationale in `Abstract_Preset` (Divi VB / wpautop and 100 * Autoptimize "Optimize HTML" both mangle the JSON when it lives in an 101 * HTML attribute). 102 * 103 * @param {Element} el Gallery wrapper element. 104 * @return {object|null} Parsed config, or null when missing/invalid. 105 */ 106 function parseConfig( el ) { 107 const scriptTag = el.querySelector( 108 ':scope > script.immomakler-gallery__config' 109 ); 110 let raw = scriptTag ? scriptTag.textContent : null; 111 if ( ! raw ) { 112 // Fallback for legacy callers still emitting the attribute. 113 raw = el.getAttribute( 'data-config' ); 114 } 115 if ( ! raw ) { 116 return null; 117 } 118 try { 119 return JSON.parse( raw ); 120 } catch ( err ) { 121 if ( typeof console !== 'undefined' && console.warn ) { 122 console.warn(
123 'immomakler-gallery: invalid gallery config', 124 el, 125 err 126 ); 127 } 128 return null; 129 } 130 } 131 132 /** 133 * Read the resolved value of `--immomakler-gallery-gap` from a gallery 134 * wrapper. Returns a non-negative number (in CSS pixels for the typical 135 * `<n>px` declaration; in whatever unit was declared otherwise â the 136 * value travels through `parseFloat` so `10px`, `10`, or `10rem` all 137 * yield `10`). 138 * 139 * Returns `null` when the var is unset or unparseable, so callers can 140 * fall back to the PHP-provided `spaceBetween` defaults. 141 * 142 * This is what powers the CSS-var â Swiper bridge: the layout gap 143 * customers tweak via `--immomakler-gallery-gap` in custom CSS gets 144 * mirrored into Swiper's `spaceBetween`, so the inter-thumb gap that 145 * Swiper computes (in JS-positioned slides) stays in sync with the 146 * grid gaps the rest of the CSS computes from the var. 147 * 148 * @param {Element} el Gallery wrapper element. 149 * @return {number|null} Resolved gap value, or null if unset/unparseable. 150 */ 151 function readGalleryGapPx( el ) { 152 return readPxVar( el, '--immomakler-gallery-gap' ); 153 } 154 155 /** 156 * Read the resolved value of `--immomakler-gallery-main-gap` from a 157 * gallery wrapper. Same semantics as `readGalleryGapPx()` but for the 158 * MAIN-SWIPER spaceBetween only. 159 * 160 * Why a separate variable: the original `--immomakler-gallery-gap` 161 * controls TWO visual concerns at once â the space between thumb cells 162 * and the space between the thumb strip and the main image. Hosts that 163 * want those independent from the space-between-main-slides (notably 164 * the Gutenberg block, where a 6 px thumb-bar gap is reasonable but a 165 * 6 px gap during a slide transition reads as visual noise) emit BOTH 166 * variables â the existing one for thumbs (and the CSS-flow gap), 167 * the new one to override the main-swiper bridge only. Hosts that 168 * don't emit `--immomakler-gallery-main-gap` see the legacy single-knob 169 * behaviour: both swipers track `--immomakler-gallery-gap` in lock-step. 170 * 171 * @param {Element} el Gallery wrapper element. 172 * @return {number|null} Resolved gap value, or null if unset/unparseable. 173 */ 174 function readGalleryMainGapPx( el ) { 175 return readPxVar( el, '--immomakler-gallery-main-gap' ); 176 } 177 178 function readPxVar( el, varName ) { 179 if ( typeof window.getComputedStyle !== 'function' ) { 180 return null; 181 } 182 const raw = window 183 .getComputedStyle( el ) 184 .getPropertyValue( varName ) 185 .trim(); 186 if ( raw === '' ) { 187 return null; 188 } 189 const num = parseFloat( raw ); 190 if ( isNaN( num ) || num < 0 ) { 191 return null; 192 } 193 return num; 194 } 195 196 /** 197 * Resolve `slidesPerView` to the slide count Swiper works with. 198 * 199 * For `slidesPerView: 'auto'` the slide width comes from CSS â in 200 * carousel mode it is the customer's carousel height multiplied by the 201 * configured aspect ratio â so neither PHP nor the JSON config can know 202 * it, and it changes with the viewport. Swiper resolves it by walking 203 * outwards from the active slide and counting until their combined size 204 * OVERFLOWS the container, which is why an exact fit still costs one 205 * more slide (hence `floor( ⦠) + 1` rather than `ceil()`). 206 * 207 * Swiper tests for that overflow only AFTER counting each slide, so a 208 * slide wider than the container still counts as two â the `max( 2, ⦠)` 209 * floor. Without it a two-image carousel on a narrow viewport would look 210 * loopable to us and not to Swiper, which is the gap this guard exists 211 * to close. 212 * 213 * Swiper can only count slides that exist, so `slideCount` caps the 214 * result. Callers weighing up whether to ADD slides pass the count they 215 * are considering rather than the count on the page â see 216 * `targetSlideCountForLoop()` for why that has to iterate. 217 * 218 * Slide sizes are summed without `spaceBetween`, matching Swiper's 219 * `swiperSlideSize`. Where slides differ in width (the "Original" 220 * aspect option lets each slide follow its own photo), the narrowest 221 * one is used: more slides fit, so the loop requirement below comes out 222 * higher, and we err towards topping up too far rather than towards 223 * re-creating the gap this guard exists to prevent. 224 * 225 * @param {Element} mainEl Main swiper container. 226 * @param {Object} config Main swiper config. 227 * @param {number} slideCount Slides to assume are present. 228 * @return {number} Slides per view, at least 1. 229 */ 230 function resolveSlidesPerView( mainEl, config, slideCount ) { 231 if ( config.slidesPerView !== 'auto' ) { 232 const fixed = Math.ceil( parseFloat( config.slidesPerView ) ); 233 return isNaN( fixed ) || fixed < 1 ? 1 : fixed; 234 } 235 236 const containerWidth = mainEl.clientWidth; 237 const slides = mainEl.querySelectorAll( '.swiper-slide' ); 238 if ( ! containerWidth || ! slides.length ) { 239 return 1; 240 } 241 242 let narrowest = Infinity;
243 slides.forEach( function ( slide ) { 244 const width = Math.ceil( slide.getBoundingClientRect().width ); 245 if ( width > 0 && width < narrowest ) { 246 narrowest = width; 247 } 248 } ); 249 if ( narrowest === Infinity ) { 250 return 1; 251 } 252 253 return Math.min( 254 slideCount, 255 Math.max( 2, Math.floor( containerWidth / narrowest ) + 1 ) 256 ); 257 } 258 259 /** 260 * How many slides Swiper's loop mode needs to work at the current size. 261 * 262 * Swiper does not fail loudly when a gallery is too short for `loop`: 263 * it logs a console warning, then leaves a slide-sized hole where a 264 * looped neighbour should be. The visitor sees fewer images than the 265 * gallery holds, and the missing one only appears after clicking 266 * "next" â the carousel layout on a 4-image property showed two images 267 * and a 284px gap. 268 * 269 * Mirrors the requirement Swiper checks in `loopFix()`: 270 * 271 * slides.length >= slidesPerView + loopedSlides 272 * 273 * loopedSlides = max( slidesPerGroup, ceil( slidesPerView / 2 ) ) // centered 274 * = slidesPerGroup // otherwise 275 * 276 * @param {Element} mainEl Main swiper container. 277 * @param {Object} config Main swiper config. 278 * @param {number} slideCount Slides to assume are present. 279 * @return {number} Slide count loop mode requires. 280 */ 281 function requiredSlidesForLoop( mainEl, config, slideCount ) { 282 const perView = resolveSlidesPerView( mainEl, config, slideCount ); 283 const group = Math.max( 1, Math.ceil( config.slidesPerGroup ) || 1 ); 284 285 let looped = config.centeredSlides 286 ? Math.max( group, Math.ceil( perView / 2 ) ) 287 : group; 288 if ( looped % group !== 0 ) { 289 looped += group - ( looped % group ); 290 } 291 looped += Math.ceil( config.loopAdditionalSlides ) || 0; 292 293 return perView + looped; 294 } 295 296 /** 297 * Slide count to grow a looping gallery to, at its current size. 298 * 299 * Adding slides can RAISE the requirement, so this cannot be answered in 300 * one pass: `slidesPerView` is capped by the slides that exist, so a 301 * gallery too short to fill its container under-reports it, and the 302 * figure only settles once enough slides are there. Four photos in a 303 * container five slides wide report a requirement of 6; at 8 slides the 304 * true requirement is 8. Adding a set at a time until the requirement 305 * stops moving converges, because the honest figure is fixed by the 306 * container width once the cap stops binding. 307 * 308 * Growth is in whole sets, so the cycle stays 1,2,3,4,1,2,3,4 rather 309 * than showing one photo twice as often as its neighbours. 310 * 311 * @param {Element} mainEl Main swiper container. 312 * @param {Object} config Main swiper config. 313 * @param {number} photoCount Slides in one set (the real photos). 314 * @param {number} currentCount Slides present now. 315 * @return {number} Slide count to reach, never below `currentCount`. 316 */ 317 function targetSlideCountForLoop( 318 mainEl, 319 config, 320 photoCount, 321 currentCount 322 ) { 323 let target = currentCount; 324 let required = requiredSlidesForLoop( mainEl, config, target ); 325 326 while ( target < required && target + photoCount <= MAX_LOOP_SLIDES ) { 327 target += photoCount; 328 required = requiredSlidesForLoop( mainEl, config, target ); 329 } 330 331 return target; 332 } 333 334 /** 335 * Clone whole sets of slides until `howMany` of them exist. 336 * 337 * @param {Array} templates Pristine slides, in their authored order. 338 * @param {number} howMany Slides to produce. 339 * @return {Array} Freshly cloned slide elements. 340 */ 341 function cloneSlideSets( templates, howMany ) { 342 const clones = []; 343 while ( clones.length < howMany ) {
344 templates.forEach( function ( slide ) { 345 clones.push( slide.cloneNode( true ) ); 346 } ); 347 } 348 return clones; 349 } 350 351 /** 352 * Make a looping gallery long enough for Swiper, or give up the loop. 353 * 354 * A carousel of four photos in a container two-and-a-half slides wide 355 * is below Swiper's threshold, so rather than leave the hole this 356 * repeats the slides until the strip is long enough. An infinite 357 * carousel of four photos shows them in the same cycle either way â 358 * 1,2,3,4,1,2,3,4 â so the repeat is invisible, and it keeps the peek 359 * on both sides that the layout is for. Dropping `loop` instead would 360 * centre the first slide against nothing and simply move the empty 361 * space to the other end. 362 * 363 * A thumbs swiper pairs with the main one slide for slide, so extra main 364 * slides would show duplicates in the thumb strip and desync the two. 365 * Those galleries drop `loop` instead, and are not watched afterwards. 366 * 367 * A gallery with no width yet (hidden in a closed tab or accordion at 368 * init) is left exactly as configured rather than guessed at â the 369 * observer wired by `watchLoopFit()` picks it up once it is shown. 370 * 371 * @param {Element} mainEl Main swiper container. 372 * @param {Object} config Main swiper config. 373 * @param {boolean} hasThumbs Whether a thumbs swiper is wired to this one. 374 * @return {{config: Object, templates: Array|null}} Config to instantiate 375 * with, plus the pristine slides to grow from later (null when 376 * this gallery must not grow). 377 */ 378 function fitConfigForLoop( mainEl, config, hasThumbs ) { 379 const wrapper = mainEl.querySelector( '.swiper-wrapper' ); 380 if ( ! wrapper || ! wrapper.children.length ) { 381 return { config, templates: null }; 382 } 383 384 const photoCount = wrapper.children.length; 385 386 if ( hasThumbs ) { 387 const required = requiredSlidesForLoop( 388 mainEl, 389 config, 390 photoCount 391 ); 392 if ( mainEl.clientWidth && photoCount < required ) { 393 return { 394 config: Object.assign( {}, config, { loop: false } ), 395 templates: null, 396 }; 397 } 398 return { config, templates: null }; 399 } 400 401 // Captured BEFORE Swiper runs: loop mode reorders the slides in the 402 // DOM, so after init the wrapper is no longer in authored order. 403 const templates = Array.prototype.slice 404 .call( wrapper.children ) 405 .map( function ( slide ) { 406 return slide.cloneNode( true ); 407 } ); 408 409 if ( mainEl.clientWidth ) { 410 const target = targetSlideCountForLoop( 411 mainEl, 412 config, 413 photoCount, 414 photoCount 415 ); 416 cloneSlideSets( templates, target - photoCount ).forEach( 417 function ( slide ) { 418 wrapper.appendChild( slide ); 419 } 420 ); 421 } 422 423 if ( ! config.pagination ) { 424 return { config, templates }; 425 } 426 427 // The repeat must not leak into the counter: it reads "n / photos", 428 // never "n / slides". `formatFractionCurrent` receives the 1-based 429 // position Swiper would have shown. Installed even when nothing has 430 // been repeated yet â the mapping is the identity until then, and a 431 // later resize can repeat the slides with the swiper already built. 432 return { 433 config: Object.assign( {}, config, { 434 pagination: Object.assign( {}, config.pagination, { 435 formatFractionCurrent( current ) { 436 return ( ( current - 1 ) % photoCount ) + 1; 437 }, 438 formatFractionTotal() { 439 return photoCount; 440 }, 441 } ), 442 } ), 443 templates, 444 }; 445 } 446 447 /** 448 * Keep a looping gallery long enough for Swiper as its width changes. 449 * 450 * How many slides `loop` needs depends on how many fit across the 451 * container, so a gallery that was long enough when it was built stops 452 * being long enough when it gets wider â a visitor rotating a tablet, a 453 * desktop window dragged out, a theme layout that widens the column when 454 * a sidebar collapses. Fixing this only at init would leave the hole 455 * waiting behind a resize. 456 * 457 * Growing is the whole repair: `appendSlide()` rebuilds the loop around 458 * the new slides, and a gallery that is already long enough for a WIDER 459 * container stays correct when it narrows, so nothing is ever removed.
460 * This also covers the gallery that had no width at init, which arrives 461 * here as the first observation once it is shown. 462 * 463 * @param {Element} mainEl Main swiper container. 464 * @param {Object} swiper The live main Swiper instance. 465 * @param {Array} templates Pristine slides to grow from. 466 * @return {void} 467 */ 468 function watchLoopFit( mainEl, swiper, templates ) { 469 if ( typeof window.ResizeObserver !== 'function' || ! templates ) { 470 return; 471 } 472 473 const photoCount = templates.length; 474 const observer = new window.ResizeObserver( function () { 475 if ( swiper.destroyed ) { 476 observer.disconnect(); 477 return; 478 } 479 if ( ! mainEl.clientWidth || ! swiper.params.loop ) { 480 return; 481 } 482 483 const current = swiper.slides.length; 484 const target = targetSlideCountForLoop( 485 mainEl, 486 swiper.params, 487 photoCount, 488 current 489 ); 490 if ( target <= current ) { 491 return; 492 } 493 494 // `appendSlide()` tears the loop down and rebuilds it, which 495 // loses the visitor's place; put them back on the photo they 496 // were looking at. 497 const wasOn = swiper.realIndex; 498 swiper.appendSlide( cloneSlideSets( templates, target - current ) ); 499 swiper.slideToLoop( wasOn, 0, false ); 500 } ); 501 502 observer.observe( mainEl ); 503 } 504 505 function instantiateSwipers( el, config ) { 506 if ( ! config || ! config.main || typeof config.main !== 'object' ) { 507 return null; 508 } 509 510 const mainSelector = config.main.selector || '.swiper'; 511 const mainEl = el.querySelector( mainSelector ); 512 if ( ! mainEl ) { 513 return null; 514 } 515 516 const Swiper = getSwiper(); 517 if ( typeof Swiper !== 'function' ) { 518 return null; 519 } 520 521 let mainConfig = config.main.config || {}; 522 let thumbsConfig = ( config.thumbs && config.thumbs.config ) || {}; 523 524 // CSS-var â Swiper bridge. The PHP preset already sets `spaceBetween` 525 // from the gap option, and the same value rides down to 526 // `--immomakler-gallery-gap` as an inline style on this wrapper. If 527 // the customer has overridden the var via their own CSS (responsive 528 // media queries, theme tweaks, â¦), `getComputedStyle()` returns the 529 // resolved value AFTER that cascade â so we honour it. 530 // 531 // `--immomakler-gallery-main-gap` overrides the main-swiper 532 // spaceBetween INDEPENDENTLY of the thumbs gap â used by hosts that 533 // want a tight main slideshow (carousel transitions, gap-less 534 // peek-carousel) without losing the thumb-strip's breathing room. 535 // When it's not set, the main swiper falls back to the 536 // `--immomakler-gallery-gap` value (legacy single-knob behaviour 537 // stays intact for the classic skin / Divi / WPBakery). 538 const gapPx = readGalleryGapPx( el ); 539 const mainGapPx = readGalleryMainGapPx( el ); 540 541 if ( gapPx !== null ) { 542 thumbsConfig = Object.assign( {}, thumbsConfig, { 543 spaceBetween: gapPx, 544 } ); 545 if ( mainGapPx === null ) { 546 mainConfig = Object.assign( {}, mainConfig, { 547 spaceBetween: gapPx, 548 } ); 549 } 550 } 551 if ( mainGapPx !== null ) { 552 mainConfig = Object.assign( {}, mainConfig, { 553 spaceBetween: mainGapPx, 554 } ); 555 } 556 557 let hasThumbs = false; 558 if ( 559 config.thumbs && 560 typeof config.thumbs === 'object' && 561 config.thumbs.selector 562 ) { 563 const thumbsEl = el.querySelector( config.thumbs.selector ); 564 if ( thumbsEl ) { 565 const thumbsSwiper = new Swiper( thumbsEl, thumbsConfig ); 566 hasThumbs = true; 567 mainConfig = Object.assign( {}, mainConfig, { 568 thumbs: Object.assign( {}, mainConfig.thumbs || {}, { 569 swiper: thumbsSwiper, 570 } ), 571 } ); 572 } 573 } 574 575 // Loop mode needs enough slides to fill the viewport plus the block 576 // Swiper shuffles around the ends; short galleries otherwise render 577 // a gap instead of an image. The requirement moves with the 578 // container width, so the gallery is kept in fit after init too. 579 let loopTemplates = null; 580 if ( mainConfig.loop ) { 581 const fitted = fitConfigForLoop( mainEl, mainConfig, hasThumbs ); 582 mainConfig = fitted.config; 583 loopTemplates = fitted.templates; 584 } 585 586 const mainSwiper = new Swiper( mainEl, mainConfig ); 587 if ( mainConfig.loop ) { 588 watchLoopFit( mainEl, mainSwiper, loopTemplates ); 589 } 590 591 return mainSwiper; 592 } 593 594 /** 595 * Request browser fullscreen on the document root. Vendor-prefix dance 596 * for Safari, older Firefox, and IE / old Edge â `Element.requestFullscreen` 597 * has been standard on every evergreen browser for years, but the prefixed 598 * variants are zero-cost fallbacks that keep older mobile Safari working. 599 */ 600 function requestRootFullscreen() { 601 const el = document.documentElement; 602 if ( typeof el.requestFullscreen === 'function' ) { 603 el.requestFullscreen(); 604 } else if ( typeof el.webkitRequestFullscreen === 'function' ) { 605 el.webkitRequestFullscreen(); 606 } else if ( typeof el.mozRequestFullScreen === 'function' ) { 607 el.mozRequestFullScreen(); 608 } else if ( typeof el.msRequestFullscreen === 'function' ) { 609 el.msRequestFullscreen(); 610 } 611 } 612
613 /** 614 * iPad/iPhone-in-iframe workaround: when the body carries the 615 * `immomakler-iframe` class (set by callers that embed the property page 616 * inside an iframe) and the user is on iOS, the lightgallery outer 617 * container computes a wrong height from iOS Safari's broken iframe 618 * viewport, so we pin it at 700px. Targets `.lg-outer` (the lightgallery 619 * 2.x outer class) so the fix isn't tied to a specific instance number. 620 * No-op outside iOS-iframe contexts. 621 */ 622 function applyIosIframeOuterHeightFix() { 623 if ( ! document.body.classList.contains( 'immomakler-iframe' ) ) { 624 return; 625 } 626 if ( ! /iP(ad|hone|od)/.test( navigator.userAgent ) ) { 627 return; 628 } 629 const outer = document.querySelector( '.lg-outer' ); 630 if ( outer ) { 631 outer.style.height = '700px'; 632 } 633 } 634 635 function resolveLightgalleryPlugins( pluginNames ) { 636 if ( ! Array.isArray( pluginNames ) ) { 637 return []; 638 } 639 const resolved = []; 640 for ( let i = 0; i < pluginNames.length; i++ ) { 641 const name = pluginNames[ i ]; 642 if ( 643 typeof name === 'string' && 644 typeof window[ name ] !== 'undefined' 645 ) { 646 resolved.push( window[ name ] ); 647 } 648 } 649 return resolved; 650 } 651 652 /* ------------------------------------------------------------------ * 653 * Lazy lightGallery loading 654 * 655 * lightGallery (core ~48 KB + plugins + ~31 KB CSS) is a lightbox: it 656 * only does anything once a visitor interacts with a gallery, and many 657 * visitors never do. So on the frontend we DON'T enqueue it up front â 658 * PHP hands us a URL map via `window.immomaklerLightgalleryAssets` and we 659 * fetch the scripts/stylesheet on demand the first time someone opens (or 660 * shows intent to open) a gallery. See Asset_Enqueuer in PHP. 661 * 662 * When lightGallery is ALREADY present (`window.lightGallery` is a 663 * function â the block editor, or a host that enqueues it eagerly), every 664 * ensure* helper resolves immediately and nothing is injected, so the 665 * classic synchronous path is preserved untouched. 666 * ------------------------------------------------------------------ */ 667 668 // Module-level singletons so multiple galleries on one page share a single 669 // download per asset. Each is created at most once and reused. 670 let lgCorePromise = null; 671 let lgCssPromise = null; 672 const lgPluginPromises = {}; 673 674 function getLightgalleryAssetMap() { 675 return window.immomaklerLightgalleryAssets || null; 676 } 677 678 function injectScriptOnce( src ) { 679 return new Promise( function ( resolve, reject ) { 680 const script = document.createElement( 'script' ); 681 script.src = src; 682 // Keep execution order deterministic relative to other injected 683 // scripts (core must run before the plugins that extend it); the 684 // promise chain in `ensureLightgallery` already enforces that, but 685 // `async = false` is belt-and-braces. 686 script.async = false; 687 script.setAttribute( 'data-immomakler-lazy-src', src ); 688 script.onload = function () { 689 resolve(); 690 }; 691 script.onerror = function () { 692 reject( 693 new Error( 694 'immomakler-gallery: failed to load ' + src 695 ) 696 ); 697 }; 698 ( document.head || document.documentElement ).appendChild( 699 script 700 ); 701 } ); 702 } 703 704 function injectStyleOnce( href ) { 705 return new Promise( function ( resolve ) { 706 const link = document.createElement( 'link' ); 707 link.rel = 'stylesheet'; 708 link.href = href; 709 link.setAttribute( 'data-immomakler-lazy-src', href ); 710 // Resolve on load OR error â a missing stylesheet must not block 711 // the lightbox from opening (it would just look unstyled). 712 link.onload = function () { 713 resolve(); 714 }; 715 link.onerror = function () { 716 resolve(); 717 }; 718 ( document.head || document.documentElement ).appendChild( link ); 719 } ); 720 } 721 722 function ensureLightgalleryCss() { 723 if ( lgCssPromise ) { 724 return lgCssPromise; 725 } 726 const map = getLightgalleryAssetMap(); 727 if ( ! map || ! map.css ) { 728 // No map (eager path / editor) or no CSS url â nothing to inject. 729 lgCssPromise = Promise.resolve(); 730 return lgCssPromise; 731 } 732 lgCssPromise = injectStyleOnce( map.css ); 733 return lgCssPromise; 734 } 735 736 function ensureLightgalleryCore() { 737 if ( typeof window.lightGallery === 'function' ) { 738 return Promise.resolve(); 739 } 740 if ( lgCorePromise ) { 741 return lgCorePromise; 742 } 743 const map = getLightgalleryAssetMap(); 744 if ( ! map || ! map.core ) { 745 return Promise.reject( 746 new Error( 747 'immomakler-gallery: lightGallery asset map missing; cannot lazy-load lightbox' 748 ) 749 ); 750 } 751 lgCorePromise = injectScriptOnce( map.core ); 752 return lgCorePromise; 753 } 754 755 function ensureLightgalleryPlugins( tokens ) { 756 if ( ! Array.isArray( tokens ) || ! tokens.length ) { 757 return Promise.resolve(); 758 } 759 const map = getLightgalleryAssetMap(); 760 const pending = []; 761 for ( let i = 0; i < tokens.length; i++ ) { 762 const token = tokens[ i ]; 763 if ( typeof token !== 'string' ) { 764 continue; 765 } 766 // Already loaded (eager path, or a previous gallery pulled it). 767 if ( typeof window[ token ] !== 'undefined' ) { 768 continue; 769 } 770 const url = map && map.plugins ? map.plugins[ token ] : null; 771 if ( ! url ) { 772 continue; 773 } 774 if ( ! lgPluginPromises[ token ] ) { 775 lgPluginPromises[ token ] = injectScriptOnce( url ); 776 } 777 pending.push( lgPluginPromises[ token ] ); 778 } 779 return Promise.all( pending ); 780 } 781 782 /** 783 * Resolve once lightGallery core + the requested plugins + the stylesheet 784 * are all available. CSS loads in parallel with the scripts; core resolves 785 * before its plugins (they extend `window.lightGallery`). 786 * 787 * @param {Array} pluginTokens Plugin-name strings from the gallery config. 788 * @return {Promise} Settles when the lightbox can be instantiated. 789 */ 790 function ensureLightgallery( pluginTokens ) { 791 return Promise.all( [ 792 ensureLightgalleryCss(), 793 ensureLightgalleryCore().then( function () { 794 return ensureLightgalleryPlugins( pluginTokens ); 795 } ), 796 ] ); 797 } 798 799 /* ------------------------------------------------------------------ * 800 * Broken-image fallback for lightbox slides 801 * 802 * PHP ships an alternate URL per slide (`immomaklerFallbackSrc`) whenever 803 * the byte-smallest variant it picked differs from WordPress' canonical 804 * `full` URL. Image optimizers sometimes leave one of the two broken â 805 * the case that prompted this was a LiteSpeed `Accept: image/webp` 806 * rewrite answering `200 image/webp` with an empty body for a single 807 * un-scaled original, while its `-scaled` sibling was fine. lightGallery 808 * reacts to the decode failure by replacing the whole slide with its 809 * "failed to load" message, so the visitor sees an error where a working 810 * URL for the same image was one hop away. 811 * 812 * We intercept in the CAPTURE phase on document: `error` does not bubble, 813 * but it does capture, so a document-level capture listener runs before 814 * lightGallery's own listener on the `<img>` itself. Swapping `src` there 815 * and calling `stopPropagation()` keeps the slide alive â the retried 816 * load fires the `load` event lightGallery is already waiting for. One 817 * retry per image (guarded by a data attribute); a second failure falls 818 * through to lightGallery's error message as before. 819 * ------------------------------------------------------------------ */ 820 821 const lightboxFallbackBySrc = {}; 822 let lightboxFallbackListening = false;
823 824 function onLightboxImageError( event ) { 825 const img = event.target; 826 if ( 827 ! img || 828 img.tagName !== 'IMG' || 829 ! img.classList || 830 ! img.classList.contains( 'lg-object' ) || 831 img.getAttribute( 'data-immomakler-fallback-applied' ) === '1' 832 ) { 833 return; 834 } 835 const fallback = 836 lightboxFallbackBySrc[ img.getAttribute( 'src' ) ] || 837 lightboxFallbackBySrc[ img.src ]; 838 if ( ! fallback ) { 839 return; 840 } 841 img.setAttribute( 'data-immomakler-fallback-applied', '1' ); 842 // Must happen before lightGallery's target-phase handler wipes the slide. 843 event.stopPropagation(); 844 // A srcset would otherwise keep winning over the src we just set. 845 img.removeAttribute( 'srcset' ); 846 img.setAttribute( 'src', fallback ); 847 } 848 849 /** 850 * Harvest `immomaklerFallbackSrc` off the gallery items into the module-level 851 * lookup the error handler reads, and return items stripped of that custom 852 * key so the payload handed to lightGallery stays pure lightGallery data. 853 * 854 * @param {Array} dynamicEl Gallery items from the `lightgallery` config block. 855 * @return {Array} The same items, minus our custom key. 856 */ 857 function extractLightboxFallbacks( dynamicEl ) { 858 let found = false; 859 const items = dynamicEl.map( function ( item ) { 860 if ( 861 ! item || 862 typeof item !== 'object' || 863 ! item.immomaklerFallbackSrc 864 ) { 865 return item; 866 } 867 const copy = Object.assign( {}, item ); 868 if ( copy.src ) { 869 lightboxFallbackBySrc[ copy.src ] = copy.immomaklerFallbackSrc; 870 found = true; 871 } 872 delete copy.immomaklerFallbackSrc; 873 return copy; 874 } ); 875 if ( found && ! lightboxFallbackListening ) { 876 lightboxFallbackListening = true; 877 document.addEventListener( 'error', onLightboxImageError, true ); 878 } 879 return items; 880 } 881 882 /** 883 * Create a lightGallery instance bound to a hidden host inside `el` and 884 * wire the legacy fullscreen / fullscreen-exit / iOS-iframe behaviour. 885 * Assumes `window.lightGallery` is available (callers guarantee it via 886 * {@see ensureLightgallery} or an eager enqueue). 887 * 888 * @param {Element} el Gallery wrapper. 889 * @param {object} lgConfig Resolved `lightgallery` config block. 890 * @return {object|null} The lightGallery instance, or null when unavailable. 891 */ 892 function createLightgalleryInstance( el, lgConfig ) { 893 if ( typeof window.lightGallery !== 'function' ) { 894 return null; 895 } 896 897 // Map plugin-name strings to the globals lightgallery's plugin scripts expose. 898 const options = Object.assign( {}, lgConfig ); 899 options.plugins = resolveLightgalleryPlugins( lgConfig.plugins ); 900 901 // Strip our custom non-lightgallery flags before handing off â keep 902 // them as locals to wire up our own listeners below. Lightgallery 903 // would ignore unknown keys anyway, but stripping keeps the surface 904 // clean for anyone debugging the call. 905 const openInFullscreen = !! options.immomaklerOpenInFullscreen; 906 delete options.immomaklerOpenInFullscreen; 907 if ( Array.isArray( options.dynamicEl ) ) { 908 options.dynamicEl = extractLightboxFallbacks( options.dynamicEl ); 909 } 910 911 // Lightgallery in dynamic mode needs a host element to attach to. 912 const host = document.createElement( 'div' ); 913 host.className = 'immomakler-gallery__lightgallery-host'; 914 host.style.display = 'none'; 915 el.appendChild( host ); 916 917 const lg = window.lightGallery( host, options ); 918 919 // Legacy behaviour preserved on the new code path. Three pieces, 920 // all hung off the host's `lgAfterOpen` / `lgAfterClose` events 921 // (so each gallery instance manages its own lifecycle and we don't 922 // leak document-level handlers between opens): 923 // 924 // 1. `immomakler_lightgallery_openinfullscreenmode` filter â 925 // request browser fullscreen on the document root when the 926 // lightbox opens (customers who want the immersive look). 927 // 928 // 2. Document-level `fullscreenchange` handler that closes the 929 // gallery when fullscreen exits. Without this, a customer 930 // who opened the gallery in fullscreen (or entered manually 931 // via the lightgallery fullscreen button) and then hits Esc 932 // to exit fullscreen would still see the lightbox sitting 933 // there over the page â they'd need a second Esc to close 934 // the gallery itself. With this, fullscreen-exit closes 935 // everything in one keystroke. 936 // 937 // 3. iPad/iPhone iframe workaround â when the body carries the 938 // `immomakler-iframe` class and the user is on iOS, force 939 // the lightgallery outer container to 700px tall. iOS Safari 940 // computes viewport height incorrectly inside iframes; the 941 // fixed height keeps the lightbox usable. 942 const fullscreenExitHandler = function () { 943 if ( 944 ! document.fullscreenElement && 945 ! document.webkitFullscreenElement && 946 ! document.mozFullScreenElement && 947 ! document.msFullscreenElement 948 ) { 949 lg.closeGallery(); 950 } 951 }; 952 host.addEventListener( 'lgAfterOpen', function () { 953 if ( openInFullscreen ) { 954 requestRootFullscreen(); 955 } 956 applyIosIframeOuterHeightFix(); 957 document.addEventListener( 'fullscreenchange', fullscreenExitHandler ); 958 document.addEventListener( 'webkitfullscreenchange', fullscreenExitHandler ); 959 document.addEventListener( 'mozfullscreenchange', fullscreenExitHandler ); 960 document.addEventListener( 'MSFullscreenChange', fullscreenExitHandler ); 961 } ); 962 host.addEventListener( 'lgAfterClose', function () { 963 document.removeEventListener( 'fullscreenchange', fullscreenExitHandler ); 964 document.removeEventListener( 'webkitfullscreenchange', fullscreenExitHandler ); 965 document.removeEventListener( 'mozfullscreenchange', fullscreenExitHandler ); 966 document.removeEventListener( 'MSFullscreenChange', fullscreenExitHandler ); 967 } ); 968 969 return lg; 970 } 971 972 /** 973 * Wire a gallery's lightbox triggers and instantiate lightGallery. 974 * 975 * Two paths share one click-handling surface: 976 * 977 * - Eager: `window.lightGallery` is already loaded (the block editor, or 978 * a host that enqueues it up front) â instantiate immediately, so the 979 * first click opens synchronously and `immomakler-gallery:ready` 980 * reports the instance. 981 * - Lazy: lightGallery isn't loaded â wire the click handlers now (cheap) 982 * and fetch the assets via {@see ensureLightgallery} on first open 983 * intent, instantiating then. A pointerenter / touchstart / focus 984 * preload warms the download so the first real click feels instant. 985 * 986 * @param {Element} el Gallery wrapper. 987 * @param {object|null} mainSwiper Main Swiper instance (swiper presets) or null. 988 * @param {object}
988 lgConfig Resolved `lightgallery` config block. 989 * @param {string} mainSelector Selector for the main swiper element. 990 * @return {object|null} The lightGallery instance when created eagerly, else null. 991 */ 992 function instantiateLightgallery( el, mainSwiper, lgConfig, mainSelector ) { 993 if ( ! lgConfig ) { 994 return null; 995 } 996 997 // Eager when lightGallery is already on the page; otherwise we need a 998 // URL map to lazy-load it. With neither, there is nothing to wire. 999 const canLazyLoad = !! getLightgalleryAssetMap(); 1000 if ( typeof window.lightGallery !== 'function' && ! canLazyLoad ) { 1001 return null; 1002 } 1003 1004 let lg = 1005 typeof window.lightGallery === 'function' 1006 ? createLightgalleryInstance( el, lgConfig ) 1007 : null; 1008 1009 const pluginTokens = lgConfig.plugins; 1010 1011 // Open the lightbox at `idx`, lazy-loading + instantiating on first use. 1012 function openAt( idx ) { 1013 // A short looping carousel repeats its slides to reach the count 1014 // Swiper's loop mode needs, so `realIndex` can run past the last 1015 // photo. The lightbox always holds one entry per photo, so fold 1016 // the index back into that range. 1017 const itemCount = Array.isArray( lgConfig.dynamicEl ) 1018 ? lgConfig.dynamicEl.length 1019 : 0; 1020 if ( itemCount > 0 ) { 1021 idx = ( ( idx % itemCount ) + itemCount ) % itemCount; 1022 } 1023 if ( lg ) { 1024 lg.openGallery( idx ); 1025 return; 1026 } 1027 ensureLightgallery( pluginTokens ) 1028 .then( function () { 1029 if ( ! lg ) { 1030 lg = createLightgalleryInstance( el, lgConfig ); 1031 } 1032 if ( lg ) { 1033 lg.openGallery( idx ); 1034 } 1035 } ) 1036 .catch( function ( err ) { 1037 if ( window.console && window.console.warn ) { 1038 window.console.warn( 1039 'immomakler-gallery: lightbox failed to load', 1040 err 1041 ); 1042 } 1043 } ); 1044 } 1045 1046 // Warm the download as soon as the visitor shows intent (hover / touch 1047 // / keyboard focus), so the first real click does not wait on the 1048 // network. Only needed on the lazy path â no-op once loaded. 1049 if ( ! lg ) { 1050 const preload = function () { 1051 ensureLightgallery( pluginTokens ).catch( function () {} ); 1052 }; 1053 el.addEventListener( 'pointerenter', preload, { once: true } ); 1054 el.addEventListener( 'touchstart', preload, { 1055 once: true, 1056 passive: true, 1057 } ); 1058 el.addEventListener( 'focusin', preload, { once: true } ); 1059 } 1060 1061 if ( mainSwiper ) { 1062 // Swiper presets: wire click on the main swiper's slides (not 1063 // the nav buttons) â open the lightbox at the currently active 1064 // slide. 1065 const mainEl = el.querySelector( mainSelector || '.swiper' ); 1066 if ( mainEl ) { 1067 mainEl.addEventListener( 'click', function ( event ) { 1068 if ( 1069 event.target.closest( 1070 '.swiper-button-prev, .swiper-button-next' 1071 ) 1072 ) { 1073 return; 1074 } 1075 if ( ! event.target.closest( '.swiper-slide' ) ) { 1076 return; 1077 } 1078 // `realIndex` is the index into the original slide 1079 // list, independent of any duplicate slides Swiper 1080 // inserts when `loop: true` is enabled. In non-loop 1081 // mode it equals `activeIndex`, so this is a strict 1082 // superset of the old behaviour. 1083 const idx = 1084 typeof mainSwiper.realIndex === 'number' 1085 ? mainSwiper.realIndex 1086 : mainSwiper.activeIndex; 1087 openAt( idx ); 1088 } ); 1089 } 1090 } else { 1091 // Static presets: wire click on any element carrying 1092 // `data-immomakler-gallery-lightbox-index="N"` â open the 1093 // lightbox at index N. Static preset markup attaches the data 1094 // attribute to the main image, each thumb, and the "View all" 1095 // button so the lightbox can open at the right starting slide. 1096 el.addEventListener( 'click', function ( event ) { 1097 const trigger = event.target.closest( 1098 '[data-immomakler-gallery-lightbox-index]' 1099 ); 1100 if ( ! trigger || ! el.contains( trigger ) ) { 1101 return; 1102 } 1103 event.preventDefault(); 1104 const raw = trigger.getAttribute( 1105 'data-immomakler-gallery-lightbox-index' 1106 ); 1107 let idx = parseInt( raw, 10 ); 1108 if ( isNaN( idx ) || idx < 0 ) { 1109 idx = 0; 1110 } 1111 openAt( idx ); 1112 } ); 1113 } 1114 1115 return lg; 1116 } 1117 1118 /** 1119 * For the vertical-thumbs preset: set the gallery wrapper's height so 1120 * the main image area renders at the customer-configured aspect ratio. 1121 * 1122 * Pure-CSS approaches all run into the same wall â CSS Grid sizes auto 1123 * row tracks by items' max-content heights, and the thumbs swiper's 1124 * max-content (sum of all stacked slides) dominates row sizing or 1125 * forces the entire wrapper to follow main_aspect (including the 1126 * thumbs column, making the actual main image squarer than configured). 1127 * Setting the wrapper height in JS based on the main column's actual 1128 * rendered width gives the main image its exact configured aspect 1129 * ratio, regardless of viewport size. 1130 * 1131 * Re-runs on viewport / container resize via ResizeObserver. 1132 * @param el 1133 */ 1134 function syncVerticalThumbsHeight( el ) { 1135 if ( 1136 ! el.classList.contains( 'immomakler-gallery--vertical_thumbs' ) 1137 ) { 1138 return; 1139 } 1140 1141 const mainEl = el.querySelector( '.immomakler-gallery__main' ); 1142 if ( ! mainEl ) { 1143 return; 1144 } 1145 1146 const thumbsEl = el.querySelector( '.immomakler-gallery__thumbs' ); 1147 1148 // Natural stacked height of the thumb rail, measured lazily once and 1149 // then cached. The thumb width is a fixed pixel custom property 1150 // (`--immomakler-gallery-vertical-thumb-width`), so each thumb's 1151 // height â and therefore the stacked total â is constant regardless 1152 // of container width; only the rail height (derived from the main 1153 // column width) varies. Caching a single measurement avoids 1154 // re-measuring after the rail is hidden (a hidden rail measures 0,
1155 // which would otherwise latch the underflow state permanently). 1156 let stackedThumbsHeight = 0; 1157 1158 function measureStackedThumbs() { 1159 if ( ! thumbsEl ) { 1160 return 0; 1161 } 1162 const slides = thumbsEl.querySelectorAll( '.swiper-slide' ); 1163 if ( ! slides.length ) { 1164 return 0; 1165 } 1166 let total = 0; 1167 slides.forEach( function ( slide ) { 1168 // `offsetHeight` is layout height, immune to Swiper's 1169 // translate transform (which is 0 at init anyway). 1170 total += slide.offsetHeight; 1171 } ); 1172 // Swiper sets `margin-bottom: spaceBetween` on vertical slides; 1173 // read it off the first slide rather than re-plumbing thumb_space. 1174 const gap = 1175 parseFloat( getComputedStyle( slides[ 0 ] ).marginBottom ) || 0; 1176 total += gap * ( slides.length - 1 ); 1177 return total; 1178 } 1179 1180 function parseAspect( str ) { 1181 const parts = ( str || '' ).split( '/' ); 1182 let x = parseFloat( parts[ 0 ] ); 1183 let y = parts.length > 1 ? parseFloat( parts[ 1 ] ) : 1; 1184 if ( ! isFinite( x ) || x <= 0 ) { 1185 x = 4; 1186 } 1187 if ( ! isFinite( y ) || y <= 0 ) { 1188 y = 3; 1189 } 1190 return { x, y }; 1191 } 1192 1193 function sync() { 1194 const mainWidth = mainEl.getBoundingClientRect().width; 1195 if ( mainWidth <= 0 ) { 1196 return; 1197 } 1198 const aspectStr = getComputedStyle( el ).getPropertyValue( 1199 '--immomakler-gallery-main-aspect' 1200 ); 1201 const aspect = parseAspect( aspectStr.trim() ); 1202 const railHeight = ( mainWidth * aspect.y ) / aspect.x; 1203 el.style.height = railHeight + 'px'; 1204 1205 // Full-width fallback for a too-short thumb rail (typically 2-4 1206 // images): if the stacked thumbs can't fill the rail, hide it and 1207 // let the main image span the whole wrapper. Measure while the 1208 // rail is still visible (before the class is applied), then just 1209 // compare the cached height on subsequent resyncs. 1210 if ( thumbsEl ) { 1211 if ( 1212 ! stackedThumbsHeight && 1213 ! el.classList.contains( 1214 'immomakler-gallery--thumbs-underflow' 1215 ) 1216 ) { 1217 stackedThumbsHeight = measureStackedThumbs(); 1218 } 1219 if ( stackedThumbsHeight > 0 ) { 1220 // Collapse only when stacked thumbs fill less than half 1221 // the rail â a gap this large makes the strip look broken. 1222 // The old threshold (railHeight - 1) was too aggressive: 1223 // it collapsed a 2-image strip on wide monitors simply 1224 // because the last thumb didn't reach the very bottom. 1225 el.classList.toggle( 1226 'immomakler-gallery--thumbs-underflow', 1227 stackedThumbsHeight < railHeight * 0.5 1228 ); 1229 } 1230 } 1231 } 1232 1233 // Initial sync. 1234 sync(); 1235 1236 // Re-sync on container resize. A `Window resize` listener is the 1237 // fallback when ResizeObserver is unavailable. 1238 if ( typeof ResizeObserver === 'function' ) { 1239 const ro = new ResizeObserver( sync ); 1240 ro.observe( el ); 1241 } else if ( 1242 typeof window !== 'undefined' && 1243 typeof window.addEventListener === 'function' 1244 ) { 1245 window.addEventListener( 'resize', sync ); 1246 } 1247 } 1248 1249 /** 1250 * Announce that a gallery is ready for custom code hooking the event. 1251 * 1252 * Both arguments are null for a gallery the loader had nothing to 1253 * construct for â subscribers get told it is ready either way, so a 1254 * listener does not wait forever on a gallery that will never fire. 1255 * 1256 * @param {Element} el Gallery wrapper element. 1257 * @param {object|null} mainSwiper The main Swiper instance, if any. 1258 * @param {object|null} lightgallery The lightGallery instance, if any. 1259 */ 1260 function dispatchReady( el, mainSwiper, lightgallery ) { 1261 el.dispatchEvent( 1262 new CustomEvent( 'immomakler-gallery:ready', { 1263 detail: { swiper: mainSwiper, lightgallery }, 1264 bubbles: true, 1265 } ) 1266 ); 1267 } 1268 1269 function initOne( el ) { 1270 if ( ! el || isInitialised( el ) ) { 1271 return; 1272 } 1273 1274 const config = parseConfig( el ); 1275 if ( ! config ) { 1276 // Nothing for the loader to construct â and nothing a later 1277 // retry could change, because the config payload is inline 1278 // server-rendered markup rather than something that arrives 1279 // with a script. Two ways to get here, both terminal: 1280 // 1281 // - A static preset whose click behaviour is not the 1282 // lightbox ("Zur Immobilie" / "Nichts"): it needs no
1283 // Swiper and no lightGallery, so it emits no payload. 1284 // - A malformed payload, which `parseConfig` has already 1285 // warned about. 1286 // 1287 // Mark it done. Leaving it unmarked is what kept 1288 // `hasUnitializedGalleries()` permanently true, so the 1289 // deferred-deps fallback poll ran all 60 of its ticks (~30 s) 1290 // on every page carrying such a gallery, re-parsing the same 1291 // markup each time. 1292 markInitialised( el ); 1293 dispatchReady( el, null, null ); 1294 return; 1295 } 1296 1297 let mainSwiper = null; 1298 if ( config.main ) { 1299 // Swiper preset: Swiper is required at runtime. If it hasn't 1300 // loaded yet, bail without marking the element initialised â 1301 // a later rescan can succeed once Swiper becomes available. 1302 if ( typeof getSwiper() !== 'function' ) { 1303 return; 1304 } 1305 mainSwiper = instantiateSwipers( el, config ); 1306 if ( ! mainSwiper ) { 1307 return; 1308 } 1309 } 1310 1311 let lg = null; 1312 if ( config.lightgallery ) { 1313 lg = instantiateLightgallery( 1314 el, 1315 mainSwiper, 1316 config.lightgallery, 1317 config.main && config.main.selector 1318 ); 1319 } 1320 1321 // Static presets exit here: no swiper instance, no vertical-thumbs 1322 // height to sync. Mark initialised and dispatch the ready event so 1323 // custom-code hookers that listen still get notified. 1324 if ( mainSwiper ) { 1325 syncVerticalThumbsHeight( el ); 1326 } 1327 1328 markInitialised( el ); 1329 dispatchReady( el, mainSwiper, lg ); 1330 } 1331 1332 function initAll( root ) { 1333 const scope = root || document; 1334 const nodes = scope.querySelectorAll( SELECTOR ); 1335 for ( let i = 0; i < nodes.length; i++ ) { 1336 initOne( nodes[ i ] ); 1337 } 1338 } 1339 1340 function ready( fn ) { 1341 if ( document.readyState !== 'loading' ) { 1342 fn(); 1343 } else { 1344 document.addEventListener( 'DOMContentLoaded', fn ); 1345 } 1346 } 1347 1348 /** 1349 * Detect galleries that still need initialization (Swiper / lightGallery 1350 * not yet loaded when `initAll` first ran, so `initOne` bailed without 1351 * marking the element ready). 1352 */ 1353 function hasUnitializedGalleries() { 1354 const nodes = document.querySelectorAll( SELECTOR ); 1355 for ( let i = 0; i < nodes.length; i++ ) { 1356 if ( ! isInitialised( nodes[ i ] ) ) { 1357 return true; 1358 } 1359 } 1360 return false; 1361 } 1362 1363 /** 1364 * Retry initialization when optimization-plugin-deferred dependencies 1365 * arrive late. 1366 * 1367 * Plugins like WP-Rocket's "Delay JavaScript execution" hold every 1368 * script (including Swiper and lightGallery) until the first user 1369 * interaction. The gallery loader itself is also delayed, but it runs 1370 * before its deps because they're declared at file-position later in 1371 * the page or in a separate "delayed" group. `initOne` correctly 1372 * bails (without marking the element ready) when the Swiper 1373 * constructor (see `getSwiper`) isn't available yet â this retry path 1374 * picks it up afterwards. 1375 * 1376 * We listen for everything that signals "more scripts have loaded": 1377 * 1378 * - `load`: native, fires once all resources have loaded. 1379 * - `rocket-DOMContentLoaded` + `rocket-load`: WP-Rocket-specific 1380 * events fired after the delay-execution batch has run. 1381 * - Autoptimize / Perfmatters don't dispatch dedicated events; 1382 * a short polling fallback (every 500ms for ~30s) covers them. 1383 * 1384 * The retry stops as soon as every gallery is initialised, or after 1385 * the timeout. Cheap to leave running â `hasUnitializedGalleries()` 1386 * is a single `querySelectorAll` + a small loop. 1387 */ 1388 function setupDeferredDepsRetry() { 1389 let retryTriggered = false; 1390 1391 function retry() { 1392 if ( retryTriggered ) { 1393 return; 1394 } 1395 retryTriggered = true; 1396 initAll(); 1397 } 1398 1399 function maybeRetry() { 1400 if ( hasUnitializedGalleries() ) { 1401 retryTriggered = false; // Allow another retry on the next event. 1402 retry(); 1403 } 1404 } 1405 1406 window.addEventListener( 'load', maybeRetry ); 1407 document.addEventListener( 'rocket-DOMContentLoaded', maybeRetry ); 1408 document.addEventListener( 'rocket-load', maybeRetry ); 1409 1410 // Fallback poll for optimization plugins that don't fire a 1411 // dedicated event (Autoptimize, Perfmatters). 500 ms à 60 ticks 1412 // = 30 s ceiling; the typical delayed-script batch finishes 1413 // loading within 1â2 ticks of first user interaction. 1414 let ticks = 0; 1415 const poll = setInterval( function () { 1416 ticks++; 1417 if ( ! hasUnitializedGalleries() ) { 1418 clearInterval( poll ); 1419 return; 1420 } 1421 if ( ticks >= 60 ) { 1422 clearInterval( poll ); 1423 return; 1424 } 1425 retryTriggered = false;
1426 retry(); 1427 }, 500 ); 1428 } 1429 1430 ready( function () { 1431 initAll(); 1432 // If anything stayed uninitialised on the first pass (most 1433 // often: the Swiper constructor / window.lightGallery not yet 1434 // loaded because an optimization plugin is delaying them), set up 1435 // the retry mechanism. We only do this if needed â for the 1436 // common case where deps loaded fine, this code path stays 1437 // completely dormant. 1438 if ( hasUnitializedGalleries() ) { 1439 setupDeferredDepsRetry(); 1440 } 1441 } ); 1442 1443 // Public API for AJAX-injected content. 1444 window.immomakler = window.immomakler || {}; 1445 window.immomakler.gallery = { 1446 init: initAll, 1447 initOne, 1448 }; 1449 1450 document.addEventListener( 'immomakler-gallery:rescan', function ( e ) { 1451 const root = e && e.detail && e.detail.root ? e.detail.root : document; 1452 initAll( root ); 1453 } ); 1454 1455 /** 1456 * Catch galleries inserted into the DOM after the initial `ready()` run. 1457 * 1458 * Concrete cases we care about: 1459 * - The Gutenberg block editor's `<ServerSideRender>` fetches the block's 1460 * HTML over REST and injects it into the editor iframe AFTER the 1461 * iframe's `DOMContentLoaded`. The one-shot `ready()` init has already 1462 * run by then, so the injected gallery would otherwise sit dormant. 1463 * `initOne` is idempotent (WeakSet-guarded), so the observer can re-fire 1464 * on every render without constructing duplicate Swipers. 1465 * - Theme-builder partial reloads (e.g. Elementor's edit-mode swap-out 1466 * of a column's HTML) and AJAX-loaded content blocks fall in the same 1467 * bucket. Hosts that already dispatch `immomakler-gallery:rescan` 1468 * stay covered â the observer is additive. 1469 * 1470 * Cheap to leave running: the callback only fires on `childList` mutations 1471 * (not attribute / text changes), and the inner check is a single 1472 * `matches`/`querySelectorAll` against a narrow selector. We don't observe 1473 * if `MutationObserver` is unavailable â the loader's required browser 1474 * support is the IE-era baseline anyway, and IE11 has the API too, so this 1475 * fallback path is effectively unreachable. 1476 */ 1477 function watchForDynamicallyInsertedGalleries() { 1478 if ( typeof MutationObserver !== 'function' || ! document.body ) { 1479 return; 1480 } 1481 const observer = new MutationObserver( function ( mutations ) { 1482 for ( let i = 0; i < mutations.length; i++ ) { 1483 const added = mutations[ i ].addedNodes; 1484 for ( let j = 0; j < added.length; j++ ) { 1485 const node = added[ j ]; 1486 // Only inspect element nodes â skip text / comment. 1487 if ( node.nodeType !== 1 ) { 1488 continue; 1489 } 1490 if ( 1491 typeof node.matches === 'function' && 1492 node.matches( SELECTOR ) 1493 ) { 1494 initOne( node ); 1495 } 1496 if ( typeof node.querySelectorAll === 'function' ) { 1497 const inner = node.querySelectorAll( SELECTOR ); 1498 for ( let k = 0; k < inner.length; k++ ) { 1499 initOne( inner[ k ] ); 1500 } 1501 } 1502 } 1503 } 1504 } ); 1505 observer.observe( document.body, { 1506 childList: true, 1507 subtree: true, 1508 } ); 1509 } 1510 1511 ready( watchForDynamicallyInsertedGalleries ); 1512} )();
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.