PageSourceSearch

https://www.ccc-real-estate.com/wp-content/plugins/immomakler/asse…/gallery/immomakler-gallery.js?ver=5.69.0

js ccc-real-estate.com collected 2026-10-05 18:43:27 UTC 53,318 bytes, 1,512 lines download raw bytes

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.