1/** 2 * Portfolio Filter Gallery - Lightbox 3 * Vanilla JS - No jQuery dependency 4 * 5 * @package Portfolio_Filter_Gallery 6 * @version 2.0.0 7 */ 8 9(function() { 10 'use strict'; 11 12 /** 13 * Lightbox class 14 */ 15 class PFGLightbox { 16 constructor() { 17 this.isOpen = false; 18 this.currentIndex = 0; 19 this.items = []; 20 this.galleryGroup = null; 21 this.touchStartX = 0; 22 this.touchEndX = 0; 23 24 this.createLightbox(); 25 this.bindEvents(); 26 } 27 28 /** 29 * Create lightbox DOM structure 30 */ 31 createLightbox() { 32 // Safe access to i18n strings with fallbacks 33 const i18n = (typeof pfgData !== 'undefined' && pfgData.i18n) ? pfgData.i18n : {}; 34 const closeText = i18n.close || 'Close'; 35 const prevText = i18n.prev || 'Previous'; 36 const nextText = i18n.next || 'Next'; 37 38 const html = ` 39 <div class="pfg-lightbox" role="dialog" aria-modal="true" aria-label="Image lightbox"> 40 <button class="pfg-lightbox-close" aria-label="${closeText}"> 41 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 42 <path d="M18 6L6 18M6 6l12 12"/> 43 </svg> 44 </button> 45 <span class="pfg-lightbox-counter"></span> 46 <button class="pfg-lightbox-nav pfg-lightbox-nav--prev" aria-label="${prevText}"> 47 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 48 <path d="M15 18l-6-6 6-6"/> 49 </svg> 50 </button> 51 <button class="pfg-lightbox-nav pfg-lightbox-nav--next" aria-label="${nextText}"> 52 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 53 <path d="M9 18l6-6-6-6"/> 54 </svg> 55 </button> 56 <div class="pfg-lightbox-content"> 57 <img class="pfg-lightbox-image" src="" alt=""> 58 <div class="pfg-lightbox-video" style="display: none;"></div> 59 </div> 60 <div class="pfg-lightbox-caption"> 61 <h3 class="pfg-lightbox-title"></h3> 62 <p class="pfg-lightbox-description"></p> 63 </div> 64 </div> 65 `; 66 67 document.body.insertAdjacentHTML('beforeend', html); 68 69 this.lightbox = document.querySelector('.pfg-lightbox'); 70 this.closeBtn = this.lightbox.querySelector('.pfg-lightbox-close'); 71 this.prevBtn = this.lightbox.querySelector('.pfg-lightbox-nav--prev'); 72 this.nextBtn = this.lightbox.querySelector('.pfg-lightbox-nav--next'); 73 this.counter = this.lightbox.querySelector('.pfg-lightbox-counter'); 74 this.content = this.lightbox.querySelector('.pfg-lightbox-content'); 75 this.image = this.lightbox.querySelector('.pfg-lightbox-image'); 76 this.video = this.lightbox.querySelector('.pfg-lightbox-video'); 77 this.title = this.lightbox.querySelector('.pfg-lightbox-title'); 78 this.description = this.lightbox.querySelector('.pfg-lightbox-description'); 79 } 80 81 /** 82 * Bind event listeners 83 */ 84 bindEvents() { 85 // Click on lightbox trigger links 86 document.addEventListener('click', (e) => { 87 const link = e.target.closest('[data-lightbox]'); 88 if (link) { 89 e.preventDefault(); 90 this.open(link); 91 } 92 }); 93 94 // Close button 95 this.closeBtn.addEventListener('click', () => this.close()); 96 97 // Navigation 98 this.prevBtn.addEventListener('click', () => this.prev()); 99 this.nextBtn.addEventListener('click', () => this.next()); 100 101 // Click outside to close 102 this.lightbox.addEventListener('click', (e) => { 103 if (e.target === this.lightbox) { 104 this.close(); 105 } 106 }); 107 108 // Keyboard navigation 109 document.addEventListener('keydown', (e) => { 110 if (!this.isOpen) return; 111 112 switch (e.key) { 113 case 'Escape': 114 this.close(); 115 break; 116 case 'ArrowLeft': 117 this.prev(); 118 break; 119 case 'ArrowRight': 120 this.next(); 121 break; 122 } 123 }); 124 125 // Touch swipe support 126 this.lightbox.addEventListener('touchstart', (e) => { 127 this.touchStartX = e.changedTouches[0].screenX; 128 }); 129 130 this.lightbox.addEventListener('touchend', (e) => { 131 this.touchEndX = e.changedTouches[0].screenX; 132 this.handleSwipe(); 133 }); 134 } 135 136 /** 137 * Handle touch swipe 138 */ 139 handleSwipe() { 140 const threshold = 50; 141 const diff = this.touchStartX - this.touchEndX; 142 143 if (Math.abs(diff) < threshold) return; 144 145 if (diff > 0) { 146 this.next(); 147 } else { 148 this.prev(); 149 } 150 } 151 152 /** 153 * Open lightbox 154 */ 155 open(trigger) { 156 this.galleryGroup = trigger.dataset.lightbox; 157 158 // Get all items in this gallery group 159 const allItems = Array.from(document.querySelectorAll(`[data-lightbox="${this.galleryGroup}"]`)); 160 161 // Filter to only visible items (not hidden by filters or pagination) 162 // Check if parent .pfg-item has hidden classes 163 this.items = allItems.filter(item => { 164 const pfgItem = item.closest('.pfg-item'); 165 // Include if no parent pfg-item (standalone) or parent is not hidden 166 if (!pfgItem) return true; 167 return !pfgItem.classList.contains('pfg-item--hidden') && 168 !pfgItem.classList.contains('pfg-item--hiding') && 169 !pfgItem.classList.contains('pfg-item--paginated-hidden'); 170 }); 171 172 // Find current index within visible items 173 this.currentIndex = this.items.indexOf(trigger); 174 175 // If trigger not in visible items (edge case), use first item 176 if (this.currentIndex === -1) { 177 this.currentIndex = 0; 178 } 179 180 // Show lightbox 181 this.lightbox.classList.add('pfg-lightbox--open'); 182 this.isOpen = true; 183 184 // Prevent body scroll with scrollbar width compensation 185 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; 186 document.documentElement.style.setProperty('--pfg-scrollbar-width', scrollbarWidth + 'px'); 187 document.body.classList.add('pfg-lightbox-open'); 188 189 // Load current item 190 this.loadItem(this.currentIndex); 191 192 // Update navigation visibility 193 this.updateNavigation(); 194 195 // Focus management 196 this.closeBtn.focus(); 197 } 198 199 /** 200 * Close lightbox 201 */ 202 close() { 203 this.lightbox.classList.remove('pfg-lightbox--open', 'pfg-lightbox--loaded'); 204 this.isOpen = false; 205 206 // Restore body scroll 207 document.body.classList.remove('pfg-lightbox-open'); 208 document.documentElement.style.removeProperty('--pfg-scrollbar-width'); 209 210 // Clear video if playing 211 this.video.innerHTML = ''; 212 this.video.style.display = 'none'; 213 this.image.style.display = ''; 214 } 215 216 /** 217 * Go to previous item 218 */ 219 prev() { 220 if (this.currentIndex > 0) { 221 this.currentIndex--; 222 this.loadItem(this.currentIndex); 223 this.updateNavigation(); 224 } 225 } 226 227 /**
228 * Go to next item 229 */ 230 next() { 231 if (this.currentIndex < this.items.length - 1) { 232 this.currentIndex++; 233 this.loadItem(this.currentIndex); 234 this.updateNavigation(); 235 } 236 } 237 238 /** 239 * Load item by index 240 */ 241 loadItem(index) { 242 const item = this.items[index]; 243 if (!item) return; 244 245 const isVideo = item.dataset.type === 'video'; 246 const src = item.href; 247 const titleText = item.dataset.title || ''; 248 const descText = item.dataset.description || ''; 249 250 // Get gallery wrapper settings 251 const wrapper = item.closest('.pfg-gallery-wrapper'); 252 const showTitle = wrapper ? wrapper.dataset.lightboxTitle !== 'false' : true; 253 const showDesc = wrapper ? wrapper.dataset.lightboxDescription === 'true' : false; 254 255 // Show loading state 256 this.lightbox.classList.remove('pfg-lightbox--loaded'); 257 this.lightbox.classList.add('pfg-lightbox--loading'); 258 259 if (isVideo) { 260 this.loadVideo(src); 261 } else { 262 this.loadImage(src); 263 } 264 265 // Update caption based on settings 266 this.title.innerHTML = showTitle ? titleText : ''; 267 this.description.innerHTML = showDesc ? descText : ''; 268 269 // Hide caption container if both are empty 270 const captionEl = this.lightbox.querySelector('.pfg-lightbox-caption'); 271 if (captionEl) { 272 const hasContent = (showTitle && titleText) || (showDesc && descText); 273 captionEl.style.display = hasContent ? '' : 'none'; 274 } 275 276 // Update counter 277 this.counter.textContent = `${index + 1} / ${this.items.length}`; 278 } 279 280 /** 281 * Load image 282 */ 283 loadImage(src) { 284 // Clear any playing video first 285 this.video.innerHTML = ''; 286 this.video.style.display = 'none'; 287 this.image.style.display = ''; 288 289 const img = new Image(); 290 img.onload = () => { 291 this.image.src = src; 292 this.image.alt = this.title.textContent; 293 this.lightbox.classList.remove('pfg-lightbox--loading'); 294 this.lightbox.classList.add('pfg-lightbox--loaded'); 295 }; 296 img.onerror = () => { 297 this.lightbox.classList.remove('pfg-lightbox--loading'); 298 }; 299 img.src = src; 300 } 301 302 /** 303 * Load video 304 */ 305 loadVideo(url) { 306 // Hide image and clear its src to free memory 307 this.image.style.display = 'none'; 308 this.image.src = ''; 309 310 // Clear any previous video iframe before loading new one 311 this.video.innerHTML = ''; 312 this.video.style.display = ''; 313 314 const embedUrl = this.getVideoEmbedUrl(url); 315 316 if (embedUrl) { 317 this.video.innerHTML = `<iframe src="${embedUrl}" allowfullscreen allow="autoplay"></iframe>`; 318 } 319 320 this.lightbox.classList.remove('pfg-lightbox--loading'); 321 this.lightbox.classList.add('pfg-lightbox--loaded'); 322 } 323 324 /** 325 * Get video embed URL 326 */ 327 getVideoEmbedUrl(url) { 328 // YouTube 329 const youtubeMatch = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/); 330 if (youtubeMatch) { 331 return `https://www.youtube.com/embed/${youtubeMatch[1]}?autoplay=1`; 332 } 333 334 // Vimeo 335 const vimeoMatch = url.match(/vimeo\.com\/(?:.*\/)?(\d+)/); 336 if (vimeoMatch) { 337 return `https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=1`; 338 } 339 340 return null; 341 } 342 343 /** 344 * Update navigation button visibility 345 */ 346 updateNavigation() { 347 this.prevBtn.style.display = this.currentIndex > 0 ? '' : 'none'; 348 this.nextBtn.style.display = this.currentIndex < this.items.length - 1 ? '' : 'none'; 349 350 // Hide counter if single item 351 this.counter.style.display = this.items.length > 1 ? '' : 'none'; 352 } 353 } 354 355 // Initialize lightbox when DOM is ready 356 function initLightbox() { 357 // Check if any lightbox triggers exist 358 if (document.querySelector('[data-lightbox]')) { 359 new PFGLightbox(); 360 } 361 } 362 363 if (document.readyState === 'loading') { 364 document.addEventListener('DOMContentLoaded', initLightbox); 365 } else { 366 initLightbox(); 367 } 368 369 // Expose to global scope 370 window.PFGLightbox = PFGLightbox; 371 372})();
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.