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 = '×'; 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.