PageSourceSearch

https://b2hchiropractic.com/_m/static-lightbox-fix.v2.js

js b2hchiropractic.com collected 2026-10-05 18:40:57 UTC 16,558 bytes, 386 lines download raw bytes

1/*!
2 * static-lightbox-fix.js
3 * Self-contained gallery lightbox for the static (NitroPack) clone.
4 *
5 * Why: Elementor lazy-loads its lightbox via a webpack dynamic import
6 * (lightbox.<hash>.bundle.min.js). That chunk was never harvested into the
7 * static mirror, so clicking a gallery thumbnail 404s the chunk and nothing
8 * opens. This script provides a dependency-free lightbox that reuses the
9 * existing gallery anchors and cycles through ALL images (prev/next/wrap).
10 *
11 * It does NOT modify, remove, or re-crop any images. It only reads the
12 * <a data-elementor-open-lightbox> anchors already on the page.
13 */
14(function () {
15  'use strict';
16
17  function init() {
18    var anchors = Array.prototype.slice.call(
19      document.querySelectorAll('a[data-elementor-open-lightbox="yes"]')
20    );
21    if (!anchors.length) return;
22
23    // Group anchors by slideshow id (fallback: single global group).
24    var groups = {};
25    anchors.forEach(function (a) {
26      var id = a.getAttribute('data-elementor-lightbox-slideshow') || '__default__';
27      (groups[id] = groups[id] || []).push(a);
28    });
29
30    function slidesFor(id) {
31      return groups[id].map(function (a) {
32        return {
33          src: a.getAttribute('href'),
34          title: a.getAttribute('data-elementor-lightbox-title') || ''
35        };
36      });
37    }
38
39    // ---- Build overlay (once) ----
40    var overlay = document.createElement('div');
41    overlay.className = 'slbx-overlay';
42    overlay.setAttribute('role', 'dialog');
43    overlay.setAttribute('aria-modal', 'true');
44    overlay.innerHTML =
45      '<button class="slbx-btn slbx-close" aria-label="Close">×</button>' +
46      '<button class="slbx-btn slbx-prev" aria-label="Previous image">‹</button>' +
47      '<figure class="slbx-figure">' +
48        '<img class="slbx-img" alt="" />' +
49        '<figcaption class="slbx-caption"></figcaption>' +
50      '</figure>' +
51      '<button class="slbx-btn slbx-next" aria-label="Next image">›</button>' +
52      '<div class="slbx-counter"></div>';
53
54    var css = document.createElement('style');
55    css.textContent =
56      '.slbx-overlay{position:fixed;inset:0;z-index:999999;display:none;align-items:center;' +
57      'justify-content:center;background:rgba(0,0,0,.92);}' +
58      '.slbx-overlay.slbx-open{display:flex;}' +
59      '.slbx-figure{margin:0;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;' +
60      'align-items:center;justify-content:center;}' +
61      '.slbx-img{max-width:92vw;max-height:80vh;object-fit:contain;box-shadow:0 4px 30px rgba(0,0,0,.5);' +
62      'border-radius:4px;}' +
63      '.slbx-caption{color:#eaeaea;font-size:14px;margin-top:12px;text-align:center;max-width:80vw;' +
64      'font-family:inherit;}' +
65      '.slbx-btn{position:absolute;background:rgba(0,0,0,.35);color:#fff;border:none;cursor:pointer;' +
66      'line-height:1;border-radius:50%;transition:background .2s;-webkit-tap-highlight-color:transparent;}' +
67      '.slbx-btn:hover{background:rgba(255,255,255,.2);}' +
68      '.slbx-close{top:16px;right:16px;width:44px;height:44px;font-size:30px;}' +
69      '.slbx-prev,.slbx-next{top:50%;transform:translateY(-50%);width:56px;height:56px;font-size:40px;}' +
70      '.slbx-prev{left:16px;}.slbx-next{right:16px;}' +
71      '.slbx-counter{position:absolute;top:20px;left:50%;transform:translateX(-50%);color:#ddd;' +
72      'font-size:14px;font-family:inherit;}' +
73      '@media (max-width:600px){.slbx-prev,.slbx-next{width:46px;height:46px;font-size:32px;}' +
74      '.slbx-close{width:40px;height:40px;font-size:26px;top:10px;right:10px;}' +
75      '.slbx-img{max-height:74vh;}}';
76
77    document.head.appendChild(css);
78    document.body.appendChild(overlay);
79
80    var imgEl = overlay.querySelector('.slbx-img');
81    var capEl = overlay.querySelector('.slbx-caption');
82    var cntEl = overlay.querySelector('.slbx-counter');
83
84    var current = { slides: [], index: 0 };
85
86    function render() {
87      var s = current.slides[current.index];
88      if (!s) return;
89      imgEl.src = s.src;
90      imgEl.alt = s.title || '';
91      capEl.textContent = (s.title || '').replace(/-/g, ' ');
92      cntEl.textContent = (current.index + 1) + ' / ' + current.slides.length;
93    }
94    function open(slides, index) {
95      current.slides = slides;
96      current.index = index;
97      render();
98      overlay.classList.add('slbx-open');
99      document.documentElement.style.overflow = 'hidden';
100    }
101    function close() {
102      overlay.classList.remove('slbx-open');
103      document.documentElement.style.overflow = '';
104      imgEl.src = '';
105    }
106    function step(d) {
107      var n = current.slides.length;
108      current.index = (current.index + d + n) % n; // wrap-around
109      render();
110    }
111
112    overlay.querySelector('.slbx-close').addEventListener('click', close);
113    overlay.querySelector('.slbx-prev').addEventListener('click', function (e) { e.stopPropagation(); step(-1); });
114    overlay.querySelector('.slbx-next').addEventListener('click', function (e) { e.stopPropagation(); step(1); });
115    overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
116    document.addEventListener('keydown', function (e) {
117      if (!overlay.classList.contains('slbx-open')) return;
118      if (e.key === 'Escape') close();
119      else if (e.key === 'ArrowLeft') step(-1);
120      else if (e.key === 'ArrowRight') step(1);
121    });
122
123    // ---- Delegated capture-phase handler ----
124    // Bound on document (capture) so it also catches Swiper-cloned loop slides
125    // and fires before Swiper/Elementor's own click handling.
126    document.addEventListener('click', function (e) {
127      var a = e.target && e.target.closest
128        ? e.target.closest('a[data-elementor-open-lightbox="yes"]')
129        : null;
130      if (!a) return;
131      e.preventDefault();
132      e.stopImmediatePropagation();
133      var id = a.getAttribute('data-elementor-lightbox-slideshow') || '__default__';
134      // Use original (non-clone) anchors for the slide set; the group still maps
135      // by slideshow id, so cloned slides resolve to the same ordered set.
136      var slides = slidesFor(id) || slidesFor('__default__');
137      var href = a.getAttribute('href');
138      var idx = slides.findIndex(function (s) { return s.src === href; });
139      open(slides, idx < 0 ? 0 : idx);
140    }, true);
141  }
142
143  /*
144   * Elementor image-carousel widgets are Swiper-driven, but the static mirror
145   * never runs Elementor's lazy carousel-init handler (bundled in the missing
146   * webpack chunk), so the arrows are dead. Swiper v8 is already loaded on the
147   * page; we instantiate it directly per carousel using each widget's own
148   * data-settings. Idempotent: skips any carousel already initialized.
149   */
150  function initOneCarousel(w) {
151    if (w.getAttribute('data-slbx-carousel') === 'done') return true;
152    var container = w.querySelector('.swiper, .swiper-container');
153    if (!container) return false;
154    if (container.swiper) {           // a Swiper already lives here
155      bindArrowFallback(w, container.swiper);
156      w.setAttribute('data-slbx-carousel', 'done');
157      return true;
158    }
159
160    // The static mirror ships with stale Swiper state baked into the markup:
161    // `swiper-initialized` classes and leftover `swiper-slide-duplicate` clones
162    // from the original server render. Swiper's loop logic then clones the
163    // clones, producing index drift that strands navigation after one step.
164    // Strip that stale state so Swiper starts from a clean, real slide set.
165    var wrapper = container.querySelector('.swiper-wrapper');
166    if (wrapper) {
167      Array.prototype.slice.call(wrapper.querySelectorAll('.swiper-slide-duplicate'))
168        .forEach(function (d) { d.parentNode.removeChild(d); });
169    }
170    container.classList.remove('swiper-initialized', 'swiper-horizontal',
171      'swiper-pointer-events', 'swiper-backface-hidden');
172
173    var cfg = {};
174    try { cfg = JSON.parse(w.getAttribute('data-settings') || '{}'); } catch (e) { cfg = {}; }
175
176    var toShow = parseInt(cfg.slides_to_show, 10) || 3;
177    var toScroll = parseInt(cfg.slides_to_scroll, 10) || 1;
178    var spacing = (cfg.image_spacing_custom && cfg.image_spacing_custom.size) || 20;
179    var realSlides = wrapper ? wrapper.querySelectorAll('.swiper-slide').length : 0;
180    var loop = cfg.infinite !== 'no' && realSlides > toShow;
181
182    var opts = {
183      initialSlide: 0,
184      slidesPerView: toShow,
185      slidesPerGroup: toScroll,
186      spaceBetween: spacing,
187      loop: loop,
188      loopAdditionalSlides: toShow + 1,
189      speed: parseInt(cfg.speed, 10) || 500,
190      watchOverflow: true,
191      navigation: {
192        nextEl: w.querySelector('.elementor-swiper-button-next, .swiper-button-next'),
193        prevEl: w.querySelector('.elementor-swiper-button-prev, .swiper-button-prev')
194      },
195      breakpoints: {
196        0:    { slidesPerView: 1, slidesPerGroup: 1 },
197        768:  { slidesPerView: Math.min(2, toShow), slidesPerGroup: 1 },
198        1025: { slidesPerView: toShow, slidesPerGroup: toScroll }
199      }
200    };
201    if (cfg.autoplay === 'yes') {
202      opts.autoplay = {
203        delay: parseInt(cfg.autoplay_speed, 10) || 5000,
204        // Never let a user click permanently kill the autoplay/nav state.
205        disableOnInteraction: false,
206        pauseOnMouseEnter: cfg.pause_on_hover === 'yes'
207      };
208    }
209
210    try {
211      var sw = new window.Swiper(container, opts);
212      bindArrowFallback(w, sw);
213      w.setAttribute('data-slbx-carousel', 'done');
214      return true;
215    } catch (e) { return false; }
216  }
217
218  // Capture-phase arrow handler that drives the instance directly. Guarantees
219  // repeated next/prev advance even if Swiper's own nav binding is ever lost,
220  // and prevents any stale Elementor handler from interfering.
221  function bindArrowFallback(w, sw) {
222    if (w.getAttribute('data-slbx-arrows') === 'done') return;
223    var next = w.querySelector('.elementor-swiper-button-next, .swiper-button-next');
224    var prev = w.querySelector('.elementor-swiper-button-prev, .swiper-button-prev');
225    function go(dir) {
226      return function (e) {
227        e.preventDefault();
228        e.stopImmediatePropagation();
229        if (sw.destroyed) return;
230        if (dir > 0) sw.slideNext(); else sw.slidePrev();
231      };
232    }
233    if (next) next.addEventListener('click', go(1), true);
234    if (prev) prev.addEventListener('click', go(-1), true);
235    w.setAttribute('data-slbx-arrows', 'done');
236  }
237
238  function initCarousels() {
239    if (typeof window.Swiper === 'undefined') return false;
240    var widgets = document.querySelectorAll('.elementor-widget-image-carousel');
241    var allDone = true;
242    Array.prototype.forEach.call(widgets, function (w) {
243      if (!initOneCarousel(w)) allDone = false;
244    });
245    return widgets.length === 0 ? true : allDone;
246  }
247
248  /*
249   * Elementor popup-trigger buttons (href="#elementor-action...popup:open...")
250   * never open on the static mirror because Elementor's popup module is in the
251   * lazy webpack chunk that 404s. The popup's real content (a booking-widget
252   * iframe) is already baked into the page inside an .elementor-popup-modal.
253   * We auto-detect that iframe's src and open it in a simple, generic modal.
254   * Portable across the fleet: nothing site-specific is hard-coded.
255   */
256  var BOOKING_RE = /https?:\/\/[^"'\s]*(?:patientcopilot\.ai|\/widget\/bookings?\/)[^"'\s]*/i;
257
258  // Synchronously find a booking iframe src already in this page's DOM.
259  function findBookingIframeSrc() {
260    var modal = document.querySelector('.elementor-popup-modal');
261    if (modal) {
262      var fi = modal.querySelector('iframe[src]');
263      if (fi && fi.getAttribute('src')) return fi.getAttribute('src');
264    }
265    var any = document.querySelector(
266      'iframe[src*="patientcopilot.ai"], iframe[src*="/widget/booking"], iframe[src*="/widget/bookings"]'
267    );
268    if (any && any.getAttribute('src')) return any.getAttribute('src');
269    // Any booking URL anywhere in the document markup (data-attrs, scripts).
270    var html = document.documentElement.innerHTML;
271    var m = html.match(BOOKING_RE);
272    return m ? m[0] : null;
273  }
274
275  // Portable cross-page fallback: many interior pages on the mirror have the
276  // popup trigger but the popup body (booking iframe) was never harvested onto
277  // them — only the homepage carries it. Fetch the homepage once, extract the
278  // booking URL, and cache it. No site-specific URL is hard-coded.
279  var _bookingFetch = null;
280  function fetchBookingSrc() {
281    if (_bookingFetch) return _bookingFetch;
282    _bookingFetch = fetch('/', { credentials: 'same-origin' })
283      .then(function (r) { return r.text(); })
284      .then(function (t) { var m = t.match(BOOKING_RE); return m ? m[0] : null; })
285      .catch(function () { return null; });
286    return _bookingFetch;
287  }
288
289  function buildPopupModal() {
290    var ov = document.createElement('div');
291    ov.className = 'slbx-pop-overlay';
292    ov.setAttribute('role', 'dialog');
293    ov.setAttribute('aria-modal', 'true');
294    ov.innerHTML =
295      '<div class="slbx-pop-box">' +
296        '<button class="slbx-pop-close" aria-label="Close">×</button>' +
297        '<div class="slbx-pop-body"></div>' +
298      '</div>';
299    var css = document.createElement('style');
300    css.textContent =
301      '.slbx-pop-overlay{position:fixed;inset:0;z-index:1000000;display:none;align-items:center;' +
302      'justify-content:center;background:rgba(0,0,0,.6);padding:20px;}' +
303      '.slbx-pop-overlay.slbx-open{display:flex;}' +
304      '.slbx-pop-box{position:relative;background:#fff;border-radius:10px;width:100%;max-width:480px;' +
305      'height:min(86vh,640px);box-shadow:0 10px 50px rgba(0,0,0,.4);overflow:hidden;}' +
306      '.slbx-pop-body,.slbx-pop-body iframe{width:100%;height:100%;border:0;display:block;}' +
307      '.slbx-pop-close{position:absolute;top:8px;right:10px;z-index:2;width:34px;height:34px;border:none;' +
308      'border-radius:50%;background:rgba(0,0,0,.45);color:#fff;font-size:24px;line-height:1;cursor:pointer;}' +
309      '.slbx-pop-close:hover{background:rgba(0,0,0,.7);}';
310    document.head.appendChild(css);
311    document.body.appendChild(ov);
312    return ov;
313  }
314
315  function initBookingPopups() {
316    var hasTriggers = document.querySelector('a[href*="elementor-action"]');
317    if (!hasTriggers) return;
318
319    var overlay = null, body = null, loaded = false;
320    function ensure() {
321      if (overlay) return;
322      overlay = buildPopupModal();
323      body = overlay.querySelector('.slbx-pop-body');
324      overlay.querySelector('.slbx-pop-close').addEventListener('click', closePop);
325      overlay.addEventListener('click', function (e) { if (e.target === overlay) closePop(); });
326      document.addEventListener('keydown', function (e) {
327        if (e.key === 'Escape' && overlay.classList.contains('slbx-open')) closePop();
328      });
329    }
330    function openPop(src) {
331      ensure();
332      if (!loaded) {
333        var ifr = document.createElement('iframe');
334        ifr.setAttribute('src', src);
335        ifr.setAttribute('title', 'Booking');
336        ifr.setAttribute('scrolling', 'yes');
337        body.appendChild(ifr);
338        loaded = true;
339      }
340      overlay.classList.add('slbx-open');
341      document.documentElement.style.overflow = 'hidden';
342    }
343    function closePop() {
344      if (!overlay) return;
345      overlay.classList.remove('slbx-open');
346      document.documentElement.style.overflow = '';
347    }
348
349    document.addEventListener('click', function (e) {
350      var a = e.target && e.target.closest
351        ? e.target.closest('a[href*="elementor-action"]')
352        : null;
353      if (!a) return;
354      var href = a.getAttribute('href') || '';
355      // Only intercept popup:open actions (leave other elementor-actions alone).
356      if (href.indexOf('popup') === -1) return;
357      e.preventDefault();
358      e.stopImmediatePropagation();
359      var src = findBookingIframeSrc();
360      if (src) { openPop(src); return; }
361      // Not on this page — fetch the booking URL from the homepage, then open.
362      fetchBookingSrc().then(function (url) {
363        if (url) openPop(url);
364        else window.open('/contact', '_blank'); // last-resort fallback
365      });
366    }, true);
367  }
368
369  function boot() {
370    init();
371    initBookingPopups();
372    if (initCarousels()) return;
373    // Swiper lib may still be loading; poll until it is ready (max ~6s).
374    var tries = 0;
375    var t = setInterval(function () {
376      tries++;
377      if (initCarousels() || tries >= 30) clearInterval(t);
378    }, 200);
379  }
380
381  if (document.readyState === 'loading') {
382    document.addEventListener('DOMContentLoaded', boot);
383  } else {
384    boot();
385  }
386})();

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.