PageSourceSearch

https://savethehippos.info/wp-content/plugins/lightbox-images-for-…/js/lightbox-images-for-divi.js?ver=2.2.6

js savethehippos.info collected 2026-10-02 22:10:13 UTC 16,856 bytes, 508 lines download raw bytes

1/**
2 * Lightbox Images for Divi Enhanced
3 *
4 * Applies lightbox to auto-linked images inside Divi text modules,
5 * covering both direct image URLs and WordPress attachment page links.
6 * Works with Divi 4 (Magnific Popup) and Divi 5 (fallback lightbox).
7 *
8 * @since 2.2.0 Rewritten in vanilla JS; jQuery/Magnific Popup optional.
9 */
10(function () {
11    'use strict';
12
13    /* ---------------------------------------------------------------
14     * 1. Configuration
15     * ------------------------------------------------------------- */
16
17    if (typeof ayudawpLightboxData === 'undefined') {
18        return;
19    }
20
21    var config          = ayudawpLightboxData;
22    var selectors       = config.selectors       || ['.entry-content a', '.et_pb_text_inner a'];
23    var imageExtensions = config.imageExtensions  || ['jpg', 'jpeg', 'gif', 'png', 'webp', 'bmp', 'svg'];
24    var debugMode       = config.debug            || false;
25
26    /* ---------------------------------------------------------------
27     * 2. Helpers
28     * ------------------------------------------------------------- */
29
30    /**
31     * Log debug messages when WP_DEBUG is active
32     */
33    function debugLog(message) {
34        if (debugMode && window.console && console.log) {
35            console.log('AyudaWP Lightbox: ' + message);
36        }
37    }
38
39    /**
40     * Build a regex from the allowed image extensions
41     */
42    var extensionPattern = new RegExp('\\.(' + imageExtensions.join('|') + ')(\\?.*)?$', 'i');
43
44    /**
45     * Check whether a URL points directly to an image file
46     */
47    function isImageUrl(url) {
48        if (!url) {
49            return false;
50        }
51        var clean = url.split('#')[0];
52        return extensionPattern.test(clean);
53    }
54
55    /**
56     * Resolve the full-size image URL for a given link.
57     *
58     * - If the href already ends in an image extension, return it.
59     * - Otherwise, only resolve from the child <img> when the link
60     *   strongly looks like a WordPress attachment page. This protects
61     *   regular linked images (buttons, CTAs, teaser images that link to
62     *   another page) from being hijacked by the lightbox.
63     */
64    function resolveImageUrl(link) {
65        var href = link.getAttribute('href') || '';
66
67        if (isImageUrl(href)) {
68            return href;
69        }
70
71        if (!isLikelyAttachmentPageLink(link)) {
72            return null;
73        }
74
75        var img = link.querySelector('img');
76        if (!img) {
77            return null;
78        }
79
80        // Try srcset first: pick the largest variant
81        var srcset = img.getAttribute('srcset');
82        if (srcset) {
83            var largest = getLargestFromSrcset(srcset);
84            if (largest) {
85                return stripSizeSuffix(largest);
86            }
87        }
88
89        // Fallback: use src and strip size suffix
90        var src = img.getAttribute('src') || '';
91        if (src) {
92            return stripSizeSuffix(src);
93        }
94
95        return null;
96    }
97
98    /**
99     * Heuristic: does this link look like a WordPress attachment page?
100     *
101     * Required: the link wraps an <img>, the href is same-origin, and the
102     * link does not open in a new window. Then accept only when either:
103     *   - the URL has an `attachment_id` query parameter, OR
104     *   - the last path segment equals (or starts with) the image file
105     *     name with the WordPress "-WIDTHxHEIGHT" suffix and the extension
106     *     stripped.
107     *
108     * Anything else (external links, generic same-origin links to unrelated
109     * pages, links opening in a new window) is treated as a plain link and
110     * left untouched.
111     */
112    function isLikelyAttachmentPageLink(link) {
113        var img = link.querySelector('img');
114        if (!img) {
115            return false;
116        }
117
118        var href = link.getAttribute('href') || '';
119        if (!href) {
120            return false;
121        }
122
123        // Attachment page links don't typically open in a new window
124        var target = (link.getAttribute('target') || '').toLowerCase();
125        if (target === '_blank') {
126            return false;
127        }
128
129        // Resolve against current location to handle relative URLs
130        var hrefUrl;
131        try {
132            hrefUrl = new URL(href, window.location.href);
133        } catch (e) {
134            return false;
135        }
136
137        // Must be same origin
138        if (hrefUrl.origin !== window.location.origin) {
139            return false;
140        }
141
142        // Explicit ?attachment_id=... is a definitive match
143        if (hrefUrl.searchParams && hrefUrl.searchParams.get('attachment_id')) {
144            return true;
145        }
146
147        // Otherwise compare the URL slug against the image file name
148        var path = hrefUrl.pathname.replace(/\/+$/, '');
149        if (!path) {
150            return false;
151        }
152        var slug = path.split('/').pop();
153        if (!slug) {
154            return false;
155        }
156
157        var src = img.getAttribute('src') || '';
158        if (!src) {
159            return false;
160        }
161
162        var fileName = src.split('/').pop().split('?')[0].split('#')[0];
163        fileName = fileName.replace(/-\d+x\d+(\.\w+)$/, '$1').replace(/\.\w+$/, '');
164        if (fileName.length < 3) {
165            return false;
166        }
167
168        var decodedSlug, decodedFileName;
169        try {
170            decodedSlug     = decodeURIComponent(slug);
171            decodedFileName = decodeURIComponent(fileName);
172        } catch (e) {
173            decodedSlug     = slug;
174            decodedFileName = fileName;
175        }
176
177        return decodedSlug === decodedFileName ||
178               decodedSlug.indexOf(decodedFileName + '-') === 0 ||
179               decodedSlug.indexOf(decodedFileName + '_') === 0;
180    }
181
182    /**
183     * Remove WordPress "-WIDTHxHEIGHT" suffix from a filename
184     * e.g. image-1024x768.jpg -> image.jpg
185     */
186    function stripSizeSuffix(url) {
187        return url.replace(/-\d+x\d+(\.\w+)(\?.*)?$/, '$1$2');
188    }
189
190    /**
191     * Find the largest image URL from a srcset attribute
192     */
193    function getLargestFromSrcset(srcset) {
194        var candidates = srcset.split(',');
195        var maxWidth   = 0;
196        var bestUrl    = '';
197
198        for (var i = 0; i < candidates.length; i++) {
199            var parts = candidates[i].trim().split(/\s+/);
200            if (parts.length < 2) {
201                continue;
202            }
203            var width = parseInt(parts[1], 10) || 0;
204            if (width > maxWidth) {
205                maxWidth = width;
206                bestUrl  = parts[0];
207            }
208        }
209        return bestUrl;
210    }
211
212    /**
213     * Check if the link is inside an allowed content area.
214     * Whitelist approach: only act inside text/blurb/post content or plain
215     * WordPress editor content. Everything else is left untouched,
216     * including Divi native modules, third-party modules, menus, etc.
217     *
218     * Order matters: allowed containers are checked BEFORE the generic
219     * .et_pb_module block, because .et_pb_post_content is itself a module.
220     */
221    function isInAllowedContext(link) {
222        // Inside allowed Divi content containers = always allowed
223        // .et_pb_post_content must be here (not below) because it has .et_pb_module too
224        if (link.closest('.et_pb_text_inner, .et_pb_blurb_content, .et_pb_post_content')) {
225            return true;
226        }
227
228        // Inside any other Divi module (native or third-party) = not allowed
229        if (link.closest('.et_pb_module')) {
230            return false;
231        }
232
233        // Inside navigation, header, footer or sidebar = not allowed
234        if (link.closest('nav, header, footer, aside, .site-header, .site-footer, .et-l--header, .et-l--footer')) {
235            return false;
236        }
237
238        // Inside general content area (WordPress editor without Divi modules) = allowed
239        if (link.closest('.entry-content')) {
240            return true;
241        }
242
243        return false;
244    }
245
246    /**
247     * Check if the link already has lightbox handling
248     */
249    function hasLightbox(link) {
250        return link.classList.contains('et_pb_lightbox_image') ||
251               link.classList.contains('et_pb_lightbox')       ||
252               link.classList.contains('magnificPopup')        ||
253               link.classList.contains('ayudawp-lightbox-ready') ||
254               link.hasAttribute('data-lightbox');
255    }
256
257    /**
258     * Check if Magnific Popup is available via jQuery
259     */
260    function hasMagnificPopup() {
261        return (typeof jQuery !== 'undefined' && typeof jQuery.fn.magnificPopup === 'function');
262    }
263
264    /* ---------------------------------------------------------------
265     * 3. Fallback lightbox (when Magnific Popup is not available)
266     * ------------------------------------------------------------- */
267
268    var fallbackOverlay = null;
269    var fallbackImage   = null;
270    var fallbackSpinner = null;
271
272    /**
273     * Create the fallback overlay DOM (once)
274     */
275    function createFallbackOverlay() {
276        if (fallbackOverlay) {
277            return;
278        }
279
280        fallbackOverlay = document.createElement('div');
281        fallbackOverlay.className = 'ayudawp-lightbox-overlay';
282        fallbackOverlay.setAttribute('role', 'dialog');
283        fallbackOverlay.setAttribute('aria-modal', 'true');
284        fallbackOverlay.setAttribute('aria-label', 'Image lightbox');
285
286        // Close button
287        var closeBtn = document.createElement('button');
288        closeBtn.className = 'ayudawp-lightbox-close';
289        closeBtn.setAttribute('type', 'button');
290        closeBtn.setAttribute('aria-label', 'Close');
291        closeBtn.innerHTML = '&times;';
292        closeBtn.addEventListener('click', closeFallbackLightbox);
293
294        // Image
295        fallbackImage = document.createElement('img');
296        fallbackImage.className = 'ayudawp-lightbox-img';
297        fallbackImage.setAttribute('alt', '');
298
299        // Loading spinner
300        fallbackSpinner = document.createElement('div');
301        fallbackSpinner.className = 'ayudawp-lightbox-spinner';
302
303        fallbackOverlay.appendChild(closeBtn);
304        fallbackOverlay.appendChild(fallbackSpinner);
305        fallbackOverlay.appendChild(fallbackImage);
306        document.body.appendChild(fallbackOverlay);
307
308        // Close on overlay background click
309        fallbackOverlay.addEventListener('click', function (e) {
310            if (e.target === fallbackOverlay) {
311                closeFallbackLightbox();
312            }
313        });
314
315        // Close on Escape key
316        document.addEventListener('keydown', function (e) {
317            if (e.key === 'Escape' || e.keyCode === 27) {
318                closeFallbackLightbox();
319            }
320        });
321    }
322
323    /**
324     * Open the fallback lightbox with a given image URL
325     */
326    function openFallbackLightbox(imageUrl) {
327        createFallbackOverlay();
328
329        fallbackImage.style.display   = 'none';
330        fallbackSpinner.style.display = 'block';
331        fallbackOverlay.classList.add('ayudawp-lightbox-active');
332        document.body.style.overflow  = 'hidden';
333
334        // Preload image
335        var tempImg = new Image();
336        tempImg.onload = function () {
337            fallbackImage.src           = imageUrl;
338            fallbackImage.style.display = 'block';
339            fallbackSpinner.style.display = 'none';
340        };
341        tempImg.onerror = function () {
342            debugLog('Failed to load image: ' + imageUrl);
343            closeFallbackLightbox();
344        };
345        tempImg.src = imageUrl;
346    }
347
348    /**
349     * Close the fallback lightbox
350     */
351    function closeFallbackLightbox() {
352        if (!fallbackOverlay) {
353            return;
354        }
355        fallbackOverlay.classList.remove('ayudawp-lightbox-active');
356        document.body.style.overflow = '';
357        fallbackImage.src = '';
358    }
359
360    /* ---------------------------------------------------------------
361     * 4. Apply lightbox to a single link
362     * ------------------------------------------------------------- */
363
364    function applyLightbox(link) {
365        var imageUrl = resolveImageUrl(link);
366
367        if (!imageUrl) {
368            debugLog('Could not resolve image URL for: ' + (link.getAttribute('href') || ''));
369            return;
370        }
371
372        if (!isInAllowedContext(link)) {
373            debugLog('Skipping link outside allowed context: ' + imageUrl);
374            return;
375        }
376
377        if (hasLightbox(link)) {
378            debugLog('Skipping already processed: ' + imageUrl);
379            return;
380        }
381
382        // The link must contain an image to be eligible
383        if (!link.querySelector('img')) {
384            debugLog('Skipping text-only link: ' + imageUrl);
385            return;
386        }
387
388        // Strategy A: Use Magnific Popup when available (best Divi integration)
389        // Always initialize directly — adding only the CSS class is not enough
390        // because Divi has already finished its initialization by this point.
391        if (hasMagnificPopup()) {
392            jQuery(link).magnificPopup({
393                type: 'image',
394                closeOnContentClick: true,
395                image: { verticalFit: true },
396                items: { src: imageUrl }
397            });
398            link.classList.add('ayudawp-lightbox-ready');
399            debugLog('Applied Magnific Popup to: ' + imageUrl);
400            return;
401        }
402
403        // Strategy B: Vanilla JS fallback lightbox
404        link.classList.add('ayudawp-lightbox-ready');
405        link.addEventListener('click', function (e) {
406            e.preventDefault();
407            openFallbackLightbox(imageUrl);
408        });
409        debugLog('Applied fallback lightbox to: ' + imageUrl);
410    }
411
412    /* ---------------------------------------------------------------
413     * 5. Process all matching links
414     * ------------------------------------------------------------- */
415
416    var combinedSelector = selectors.join(', ');
417
418    /**
419     * Debounce utility to avoid excessive re-processing
420     */
421    var processTimer = null;
422
423    function scheduleProcess() {
424        if (processTimer) {
425            clearTimeout(processTimer);
426        }
427        processTimer = setTimeout(processLinks, 150);
428    }
429
430    function processLinks() {
431        var links = document.querySelectorAll(combinedSelector);
432        var count = 0;
433
434        debugLog('Processing links with selectors: ' + combinedSelector);
435
436        for (var i = 0; i < links.length; i++) {
437            var link = links[i];
438            var href = link.getAttribute('href');
439
440            if (!href || href.charAt(0) === '#') {
441                continue;
442            }
443
444            if (/^(mailto|tel|sms|skype):/.test(href)) {
445                continue;
446            }
447
448            applyLightbox(link);
449            count++;
450        }
451
452        debugLog('Processed ' + count + ' links');
453    }
454
455    /* ---------------------------------------------------------------
456     * 6. Initialization
457     * ------------------------------------------------------------- */
458
459    function init() {
460        debugLog('Initializing AyudaWP Lightbox (v2.2.6)');
461
462        processLinks();
463
464        // Re-process when Divi loads dynamic content (Divi 4 event)
465        if (typeof jQuery !== 'undefined') {
466            jQuery(document).on('et_pb_after_page_load', function () {
467                debugLog('Divi page load detected, re-processing');
468                scheduleProcess();
469            });
470        }
471
472        // MutationObserver for dynamically added content (Divi 5 + AJAX)
473        if (window.MutationObserver) {
474            var observer = new MutationObserver(function (mutations) {
475                for (var m = 0; m < mutations.length; m++) {
476                    if (mutations[m].type !== 'childList' || !mutations[m].addedNodes.length) {
477                        continue;
478                    }
479                    for (var n = 0; n < mutations[m].addedNodes.length; n++) {
480                        var node = mutations[m].addedNodes[n];
481                        if (node.nodeType !== 1) {
482                            continue;
483                        }
484                        if (node.matches && (node.matches('a[href]') || node.querySelector('a[href]'))) {
485                            debugLog('New content detected via MutationObserver');
486                            scheduleProcess();
487                            return;
488                        }
489                    }
490                }
491            });
492
493            observer.observe(document.body, { childList: true, subtree: true });
494        }
495    }
496
497    // Wait for the DOM to be ready
498    if (document.readyState === 'loading') {
499        document.addEventListener('DOMContentLoaded', function () {
500            // Small delay to let Divi finish its setup
501            setTimeout(init, 100);
502        });
503    } else {
504        setTimeout(init, 100);
505    }
506
507    debugLog('AyudaWP Lightbox script loaded');
508})();

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.