PageSourceSearch

https://camillanylund.com/wp-content/plugins/portfolio-filter-gallery/public/js/pfg-lightbox.js?ver=2.2.0

js camillanylund.com collected 2026-10-02 12:50:12 UTC 13,678 bytes, 372 lines download raw bytes

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.