PageSourceSearch

https://alfredcurrier.com/wp-content/plugins/cm-alpha/ui-lightbox/cm-lightbox.js?ver=1772919554

js alfredcurrier.com collected 2026-10-02 14:15:33 UTC 58,519 bytes, 1,388 lines download raw bytes

1/**
2 * CM-Lightbox - A lightweight, responsive lightbox library for WordPress
3 * Version: 1.0.0
4 * Author: CM Alpha
5 */
6
7(function($, window, document) {
8    'use strict';
9    
10    // Module pattern with namespace
11    window.CMLightbox = (function() {
12        
13        // Private variables
14        var groups = new Map();
15        var activeInstance = null;
16        var defaultOptions = {
17            styles: {
18                background: 'rgba(5,5,5,0.67)',
19                backgroundTop: '#000000',
20                backgroundBottom: '#000000',
21                colorTop: '#EFEFEF',
22                colorBottom: '#f8f8f8',
23                border: 'solid thin #000000',
24                closeButton: true
25            },
26            thumbnails: true,
27            cursor: 'pointer',
28            fadeInDuration: 300,
29            fadeOutDuration: 300,
30            zIndex: 9999
31        };
32        
33        // Debug mode
34        var debugMode = false;
35        
36        function log(message, data) {
37            if (debugMode && console && console.log) {
38                console.log('[CM-Lightbox] ' + message, data || '');
39            }
40        }
41        
42        // Core Classes
43        class Group {
44            constructor(name, options) {
45                this.name = name;
46                this.members = [];
47                this.options = $.extend(true, {}, defaultOptions, options);
48                // log('Group created:', { name: name, options: this.options });
49            }
50            
51            addMember(member) {
52                this.members.push(member);
53                // log('Member added to group ' + this.name + ':', member);
54            }
55        }
56        
57        class DOMScanner {
58            constructor() {
59                this.attributePrefix = 'data-cm-lb-';
60                this.groupAttribute = 'data-cm-lb-group';
61            }
62            
63            scan(groupName) {
64                // log('Scanning DOM for group:', groupName);
65                const selector = `[${this.groupAttribute}="${groupName}"]`;
66                const elements = document.querySelectorAll(selector);
67                const members = [];
68                
69                elements.forEach((element, index) => {
70                    const attributes = this.extractAttributes(element);
71                    const member = {
72                        element: element,
73                        index: index,
74                        attributes: attributes,
75                        url: attributes.url || element.getAttribute('src') || element.getAttribute('href')
76                    };
77                    
78                    if (attributes.watermarkStatus) {
79                        console.log('[CM-Lightbox][Watermark] element', {
80                            group: groupName,
81                            index: index,
82                            status: attributes.watermarkStatus,
83                            message: attributes.watermarkMessage || '',
84                            url: member.url
85                        });
86                    }
87                    
88                    // Apply cursor style if needed
89                    if (attributes.cursor !== 'false') {
90                        element.style.cursor = 'pointer';
91                    }
92                    
93                    members.push(member);
94                });
95                
96                // log('Scan complete. Found ' + members.length + ' members');
97                return members;
98            }
99            
100            extractAttributes(element) {
101                const attributes = {};
102                const attrs = element.attributes;
103                
104                for (let i = 0; i < attrs.length; i++) {
105                    const attr = attrs[i];
106                    if (attr.name.startsWith(this.attributePrefix)) {
107                        const key = this.camelCase(attr.name.replace(this.attributePrefix, ''));
108                        attributes[key] = attr.value;
109                    }
110                }
111                
112                return attributes;
113            }
114            
115            camelCase(str) {
116                return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
117            }
118        }
119        
120        class ContentTypeDetector {
121            static detect(url, explicitType) {
122                if (!url) {
123                    // log('ContentTypeDetector: No URL provided, defaulting to html');
124                    return 'html';
125                }
126                
127                // If explicit type is provided, use it
128                if (explicitType) {
129                    // log('ContentTypeDetector: Using explicit type:', explicitType);
130                    return explicitType;
131                }
132                
133                // log('ContentTypeDetector: Detecting type for URL:', url);
134                
135                const cleanUrl = url.toLowerCase();
136                
137                // Remove query parameters and hash for extension checking
138                const urlWithoutParams = cleanUrl.split('?')[0].split('#')[0];
139                
140                // log('ContentTypeDetector: URL without params:', urlWithoutParams);
141                
142                // Check for known image services first
143                const imageServices = [
144                    'picsum.photos',
145                    'placeholder.com',
146                    'placehold.it',
147                    'lorempixel.com',
148                    'unsplash.com',
149                    'pexels.com',
150                    'imgur.com',
151                    'cloudinary.com',
152                    'imgix.net'
153                ];
154                
155                if (imageServices.some(service => cleanUrl.includes(service))) {
156                  // 'ContentTypeDetector: Detected as image (known image service)');
157                    return 'image';
158                }
159                
160                // Image detection by file extension
161                if (/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff|tif)$/i.test(urlWithoutParams)) {
162                    // log('ContentTypeDetector: Detected as image (file extension)');
163                    return 'image';
164                }
165                
166                // Check for image in path (common patterns)
167                if (/\/(image|images|img|photo|photos|picture|pictures|media|assets|static)\//i.test(urlWithoutParams)) {
168                    // Additional check for numeric endings (common for dynamic images)
169                    if (/\/\d+$/.test(urlWithoutParams) || /\/\d+x\d+$/.test(urlWithoutParams)) {
170                        // log('ContentTypeDetector: Detected as image (path pattern)');
171                        return 'image';
172                    }
173                }
174                
175                // YouTube detection
176                if (cleanUrl.includes('youtube.com') || cleanUrl.includes('youtu.be')) {
177                    return 'youtube';
178                }
179                
180                // Audio detection
181                if (/\.mp3$/i.test(urlWithoutParams)) {
182                    return 'audio';
183                }
184                
185                // PDF detection
186                if (/\.pdf$/i.test(urlWithoutParams)) {
187                    return 'pdf';
188                }
189                
190                // Google Maps detection
191                if (cleanUrl.includes('maps.google.com') ||
192                    cleanUrl.includes('google.com/maps') ||
193                    cleanUrl.includes('goo.gl/maps')) {
194                    return 'maps';
195                }
196                
197                // Default to HTML
198                // log('ContentTypeDetector: No match found, defaulting to html');
199                return 'html';
200            }
201            
202            static getYouTubeId(url) {
203                const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
204                const match = url.match(regex);
205                return match ? match[1] : null;
206            }
207        }
208        
209        class ContentRenderer {
210            static render(type, url, container, options) {
211                // log('Rendering content type:', type);
212                const renderer = this.getRenderer(type);
213                return renderer.call(this, url, container, options);
214            }
215            
216            static getRenderer(type) {
217                const renderers = {
218                    image: this.renderImage,
219                    youtube: this.renderYouTube,
220                    audio: this.renderAudio,
221                    pdf: this.renderPDF,
222                    maps: this.renderMaps,
223                    html: this.renderHTML
224                };
225                
226                return renderers[type] || renderers.html;
227            }
228            
229            static renderImage(url, container, options) {
230                const img = document.createElement('img');
231                img.src = url;
232                img.className = 'cm-lightbox-image';
233                img.dataset.zoomable = 'true';
234                
235                return new Promise((resolve, reject) => {
236                    img.onload = () => {
237                        if (console && console.log) {
238                            console.log('[CM-Lightbox][Watermark] image loaded', {
239                                src: img.src,
240                                naturalWidth: img.naturalWidth,
241                                naturalHeight: img.naturalHeight
242                            });
243                        }
244                        container.appendChild(img);
245                        resolve({
246                            type: 'image',
247                            element: img,
248                            naturalWidth: img.naturalWidth,
249                            naturalHeight: img.naturalHeight
250                        });
251                    };
252                    img.onerror = () => {
253                        log('Error loading image:', url);
254                        reject(new Error('Failed to load image'));
255                    };
256                });
257            }
258            
259            static renderYouTube(url, container, options) {
260                const videoId = ContentTypeDetector.getYouTubeId(url);
261                if (!videoId) {
262                    return this.renderHTML('Invalid YouTube URL', container, options);
263                }
264                
265                const iframe = document.createElement('iframe');
266                iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
267                iframe.className = 'cm-lightbox-youtube';
268                iframe.setAttribute('allowfullscreen', '');
269                iframe.setAttribute('frameborder', '0');
270                iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
271                
272                container.appendChild(iframe);
273                
274                return Promise.resolve({
275                    type: 'youtube',
276                    element: iframe,
277                    naturalWidth: 16,  // 16:9 aspect ratio
278                    naturalHeight: 9
279                });
280            }
281            
282            static renderAudio(url, container, options) {
283                const wrapper = document.createElement('div');
284                wrapper.className = 'cm-lightbox-audio-wrapper';
285                
286                const audio = document.createElement('audio');
287                audio.src = url;
288                audio.controls = true;
289                audio.autoplay = true;
290                
291                wrapper.appendChild(audio);
292                container.appendChild(wrapper);
293                
294                return Promise.resolve({
295                    type: 'audio',
296                    element: wrapper,
297                    naturalWidth: 400,  // Default width for audio player
298                    naturalHeight: 100
299                });
300            }
301            
302            static renderPDF(url, container, options) {
303                const iframe = document.createElement('iframe');
304                iframe.src = url;
305                iframe.className = 'cm-lightbox-pdf';
306                iframe.setAttribute('frameborder', '0');
307                
308                container.appendChild(iframe);
309                
310                return Promise.resolve({
311                    type: 'pdf',
312                    element: iframe,
313                    naturalWidth: 4,   // 4:3 aspect ratio for PDFs
314                    naturalHeight: 3
315                });
316            }
317            
318            static renderMaps(url, container, options) {
319                const iframe = document.createElement('iframe');
320                iframe.src = url;
321                iframe.className = 'cm-lightbox-maps';
322                iframe.setAttribute('frameborder', '0');
323                iframe.setAttribute('allowfullscreen', '');
324                
325                container.appendChild(iframe);
326                
327                return Promise.resolve({
328                    type: 'maps',
329                    element: iframe,
330                    naturalWidth: 16,  // 16:9 aspect ratio for maps
331                    naturalHeight: 9
332                });
333            }
334            
335            static renderHTML(content, container, options) {
336                const wrapper = document.createElement('div');
337                wrapper.className = 'cm-lightbox-html';
338                wrapper.innerHTML = content;
339                
340                container.appendChild(wrapper);
341                
342                return Promise.resolve({
343                    type: 'html',
344                    element: wrapper,
345                    naturalWidth: 4,   // Default 4:3 aspect ratio
346                    naturalHeight: 3
347                });
348            }
349        }
350        
351        class ViewportCalculator {
352            constructor() {
353                this.textPadding = 20;
354                this.contentPadding = 40;
355            }
356            
357            calculate(content, options) {
358                const viewport = this.getViewportDimensions();
359                const textHeights = this.calculateTextHeights(options);
360                
361                const availableWidth = viewport.width - this.contentPadding;
362                const availableHeight = viewport.height - textHeights.total - this.contentPadding;
363                
364                const dimensions = this.calculateOptimalDimensions(
365                    content,
366                    { width: availableWidth, height: availableHeight }
367                );
368                
369                return {
370                    content: dimensions,
371                    container: {
372                        width: viewport.width,
373                        height: viewport.height
374                    },
375                    textHeights: textHeights
376                };
377            }
378            
379            calculateOptimalDimensions(content, available) {
380                const contentAspect = content.naturalWidth / content.naturalHeight;
381                const availableAspect = available.width / available.height;
382                
383                let width, height;
384                
385                if (contentAspect > availableAspect) {
386                    // Content is wider - fit to width
387                    width = available.width;
388                    height = width / contentAspect;
389                } else {
390                    // Content is taller - fit to height
391                    height = available.height;
392                    width = height * contentAspect;
393                }
394                
395                return {
396                    width: Math.floor(width),
397                    height: Math.floor(height)
398                };
399            }
400            
401            calculateTextHeights(options) {
402                const heights = {
403                    top: 0,
404                    bottom: 0,
405                    thumbnails: 0,
406                    total: 0
407                };
408                
409                // Measure actual text heights after rendering
410                const topText = document.querySelector('.cm-lightbox-top-text');
411                const bottomText = document.querySelector('.cm-lightbox-bottom-text');
412                
413                if (options.textTop && topText) {
414                    heights.top = topText.offsetHeight || 40;
415                }
416                
417                if (options.textBottom && bottomText) {
418                    heights.bottom = bottomText.offsetHeight || 40;
419                }
420                
421                if (options.thumbnails !== 'false' && options.thumbnails !== false) {
422                    heights.thumbnails = 70; // 50px height + padding
423                }
424                
425                heights.total = heights.top + heights.bottom + heights.thumbnails + this.textPadding;
426                
427                return heights;
428            }
429            
430            getViewportDimensions() {
431                return {
432                    width: window.innerWidth || document.documentElement.clientWidth,
433                    height: window.innerHeight || document.documentElement.clientHeight
434                };
435            }
436        }
437        
438        class StyleParser {
439            parse(styleString) {
440                if (!styleString) return {};
441                
442                const styles = {};
443                const declarations = styleString.split(';');
444                
445                declarations.forEach(declaration => {
446                    const colonIndex = declaration.indexOf(':');
447                    if (colonIndex > 0) {
448                        const property = declaration.substring(0, colonIndex).trim();
449                        const value = declaration.substring(colonIndex + 1).trim();
450                        
451                        if (property && value) {
452                            // Convert property names to camelCase
453                            const camelProperty = property.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
454                            styles[camelProperty] = value;
455                        }
456                    }
457                });
458                
459                return styles;
460            }
461            
462            apply(overlay, styles) {
463                // Map custom properties to CSS
464                const propertyMap = {
465                    'background': (el, value) => {
466                        el.style.backgroundColor = value;
467                    },
468                    'backgroundTop': (el, value) => {
469                        const topText = el.querySelector('.cm-lightbox-top-text');
470                        if (topText) topText.style.backgroundColor = value;
471                    },
472                    'backgroundBottom': (el, value) => {
473                        const bottomText = el.querySelector('.cm-lightbox-bottom-text');
474                        if (bottomText) bottomText.style.backgroundColor = value;
475                    },
476                    'colorTop': (el, value) => {
477                        const topText = el.querySelector('.cm-lightbox-top-text');
478                        if (topText) topText.style.color = value;
479                    },
480                    'colorBottom': (el, value) => {
481                        const bottomText = el.querySelector('.cm-lightbox-bottom-text');
482                        if (bottomText) bottomText.style.color = value;
483                    },
484                    'border': (el, value) => {
485                        const content = el.querySelector('.cm-lightbox-content');
486                        if (content) content.style.border = value;
487                    },
488                    'closeButton': (el, value) => {
489                        const closeBtn = el.querySelector('.cm-lightbox-close');
490                        if (closeBtn) closeBtn.style.display = value === 'false' ? 'none' : 'block';
491                    }
492                };
493                
494                Object.keys(styles).forEach(property => {
495                    if (propertyMap[property]) {
496                        propertyMap[property](overlay, styles[property]);
497                    }
498                });
499            }
500        }
501        
502        class LightboxUI {
503            constructor() {
504                this.elements = {};
505            }
506            
507            build() {
508                // Create overlay
509                this.elements.overlay = this.createElement('div', 'cm-lightbox-overlay');
510                
511                // Create container
512                this.elements.container = this.createElement('div', 'cm-lightbox-container');
513                
514                // Create content structure
515                this.elements.topText = this.createElement('div', 'cm-lightbox-top-text');
516                this.elements.contentWrapper = this.createElement('div', 'cm-lightbox-content-wrapper');
517                this.elements.content = this.createElement('div', 'cm-lightbox-content');
518                this.elements.bottomText = this.createElement('div', 'cm-lightbox-bottom-text');
519                this.elements.thumbnails = this.createElement('div', 'cm-lightbox-thumbnails');
520                
521                // Build content structure
522                this.elements.contentWrapper.appendChild(this.elements.content);
523                this.elements.container.appendChild(this.elements.topText);
524                this.elements.container.appendChild(this.elements.contentWrapper);
525                this.elements.container.appendChild(this.elements.bottomText);
526                this.elements.container.appendChild(this.elements.thumbnails);
527                
528                // Create navigation
529                this.elements.prevButton = this.createElement('button', 'cm-lightbox-nav cm-lightbox-prev');
530                this.elements.prevButton.innerHTML = '&#8249;';
531                this.elements.prevButton.setAttribute('aria-label', 'Previous');
532                
533                this.elements.nextButton = this.createElement('button', 'cm-lightbox-nav cm-lightbox-next');
534                this.elements.nextButton.innerHTML = '&#8250;';
535                this.elements.nextButton.setAttribute('aria-label', 'Next');
536                
537                // Create toolbar
538                this.elements.toolbar = this.createElement('div', 'cm-lightbox-toolbar');
539                
540                this.elements.zoomButton = this.createElement('button', 'cm-lightbox-zoom');
541                this.elements.zoomButton.innerHTML = '🔍';
542                this.elements.zoomButton.setAttribute('aria-label', 'Zoom');
543                this.elements.zoomButton.setAttribute('title', 'Zoom');
544                this.elements.zoomButton.style.display = 'none'; // Hidden by default
545                
546                this.elements.fullscreenButton = this.createElement('button', 'cm-lightbox-fullscreen');
547                this.elements.fullscreenButton.innerHTML = '⛶';
548                this.elements.fullscreenButton.setAttribute('aria-label', 'Fullscreen');
549                this.elements.fullscreenButton.setAttribute('title', 'Full Screen');
550                
551                this.elements.closeButton = this.createElement('button', 'cm-lightbox-close');
552                this.elements.closeButton.innerHTML = '✕';
553                this.elements.closeButton.setAttribute('aria-label', 'Close');
554                this.elements.closeButton.setAttribute('title', 'Close');
555                
556                // Build toolbar
557                this.elements.toolbar.appendChild(this.elements.zoomButton);
558                this.elements.toolbar.appendChild(this.elements.fullscreenButton);
559                this.elements.toolbar.appendChild(this.elements.closeButton);
560                
561                // Assemble main structure
562                this.elements.overlay.appendChild(this.elements.container);
563                this.elements.overlay.appendChild(this.elements.prevButton);
564                this.elements.overlay.appendChild(this.elements.nextButton);
565                this.elements.overlay.appendChild(this.elements.toolbar);
566                
567                return this.elements.overlay;
568            }
569            
570            createElement(tag, className) {
571                const element = document.createElement(tag);
572                if (className) {
573                    element.className = className;
574                }
575                return element;
576            }
577        }
578        
579        class NavigationController {
580            constructor(lightbox) {
581                this.lightbox = lightbox;
582                this.touchStartX = 0;
583                this.touchEndX = 0;
584                this.bindEvents();
585            }
586            
587            bindEvents() {
588                // Click events
589                this.lightbox.ui.elements.prevButton.addEventListener('click', () => this.navigate(-1));
590                this.lightbox.ui.elements.nextButton.addEventListener('click', () => this.navigate(1));
591                
592                // Keyboard events
593                this.keyboardHandler = this.handleKeyboard.bind(this);
594                document.addEventListener('keydown', this.keyboardHandler);
595                
596                // Touch events for mobile
597                this.setupTouchEvents();
598            }
599            
600            unbindEvents() {
601                document.removeEventListener('keydown', this.keyboardHandler);
602            }
603            
604            navigate(direction) {
605                const currentIndex = this.lightbox.currentIndex;
606                const groupSize = this.lightbox.group.members.length;
607                let newIndex = currentIndex + direction;
608                
609                // Wrap around
610                if (newIndex < 0) {
611                    newIndex = groupSize - 1;
612                } else if (newIndex >= groupSize) {
613                    newIndex = 0;
614                }
615                
616                this.lightbox.showItem(newIndex);
617            }
618            
619            handleKeyboard(e) {
620                if (!this.lightbox.isOpen) return;
621                
622                switch(e.key) {
623                    case 'ArrowLeft':
624                        e.preventDefault();
625                        this.navigate(-1);
626                        break;
627                    case 'ArrowRight':
628                        e.preventDefault();
629                        this.navigate(1);
630                        break;
631                    case 'Escape':
632                        e.preventDefault();
633                        this.lightbox.close();
634                        break;
635                }
636            }
637            
638            setupTouchEvents() {
639                const container = this.lightbox.ui.elements.container;
640                
641                container.addEventListener('touchstart', (e) => {
642                    this.touchStartX = e.changedTouches[0].screenX;
643                }, { passive: true });
644                
645                container.addEventListener('touchend', (e) => {
646                    this.touchEndX = e.changedTouches[0].screenX;
647                    this.handleSwipe();
648                }, { passive: true });
649            }
650            
651            handleSwipe() {
652                const swipeThreshold = 50;
653                const diff = this.touchStartX - this.touchEndX;
654                
655                if (Math.abs(diff) > swipeThreshold) {
656                    if (diff > 0) {
657                        // Swipe left - next
658                        this.navigate(1);
659                    } else {
660                        // Swipe right - previous
661                        this.navigate(-1);
662                    }
663                }
664            }
665        }
666        
667        class ThumbnailGallery {
668            constructor(lightbox) {
669                this.lightbox = lightbox;
670                this.thumbnailWidth = 75;
671                this.thumbnailHeight = 50;
672            }
673            
674            build() {
675                const container = this.lightbox.ui.elements.thumbnails;
676                container.innerHTML = ''; // Clear existing
677                
678                const showThumbnails = this.lightbox.currentMember.attributes.thumbnails !== 'false' && 
679                                     this.lightbox.currentMember.attributes.thumbnails !== false;
680                
681                // Always keep thumbnails in DOM, CSS handles visibility
682                if (!showThumbnails) {
683                    container.style.display = 'none';
684                } else {
685                    container.style.display = 'block';
686                }
687                const wrapper = document.createElement('div');
688                wrapper.className = 'cm-lightbox-thumbnails-wrapper';
689                
690                this.lightbox.group.members.forEach((member, index) => {
691                    const thumb = this.createThumbnail(member, index);
692                    wrapper.appendChild(thumb);
693                });
694                
695                container.appendChild(wrapper);
696                this.updateActive(this.lightbox.currentIndex);
697            }
698            
699            createThumbnail(member, index) {
700                const thumb = document.createElement('div');
701                thumb.className = 'cm-lightbox-thumbnail';
702                thumb.dataset.index = index;
703                
704                // Detect content type for potential video overlay
705                const explicitType = member.attributes.type || member.attributes.contentType;
706                const contentType = ContentTypeDetector.detect(member.url, explicitType);
707                
708                // Find the source image within the original DOM element
709                const sourceImg = member.element.querySelector('img');
710                
711                if (sourceImg && sourceImg.src) {
712                    // Use the source image for thumbnail
713                    const img = document.createElement('img');
714                    img.src = sourceImg.src;
715                    img.style.width = this.thumbnailWidth + 'px';
716                    img.style.height = this.thumbnailHeight + 'px';
717                    img.style.objectFit = 'cover';
718                    thumb.appendChild(img);
719                    
720                    // Add video play icon overlay if content is video-related
721                    if (contentType === 'youtube' || contentType === 'video') {
722                        const playIcon = document.createElement('div');
723                        playIcon.className = 'cm-video-play-icon';
724                        playIcon.innerHTML = '';
725                        thumb.appendChild(playIcon);
726                    }
727                } else {
728                    // Fallback: if no source image found, detect content type and create thumbnail
729                    if (contentType === 'image') {
730                        const img = document.createElement('img');
731                        img.src = member.url;
732                        img.style.width = this.thumbnailWidth + 'px';
733                        img.style.height = this.thumbnailHeight + 'px';
734                        img.style.objectFit = 'cover';
735                        thumb.appendChild(img);
736                    } else {
737                        // Placeholder for non-image content
738                        thumb.innerHTML = `<div class="cm-lightbox-thumbnail-placeholder">${contentType}</div>`;
739                        
740                        // Add play icon for video placeholders too
741                        if (contentType === 'youtube' || contentType === 'video') {
742                            const playIcon = document.createElement('div');
743                            playIcon.className = 'cm-lightbox-thumbnail-play-icon';
744                            playIcon.innerHTML = '▶';
745                            playIcon.style.cssText = `
746                                position: absolute;
747                                top: 50%;
748                                left: 50%;
749                                transform: translate(-50%, -50%);
750                                background: rgba(255, 255, 255, 0.9);
751                                color: #333;
752                                border-radius: 50%;
753                                width: 24px;
754                                height: 24px;
755                                display: flex;
756                                align-items: center;
757                                justify-content: center;
758                                font-size: 12px;
759                                pointer-events: none;
760                                z-index: 1;
761                            `;
762                            thumb.style.position = 'relative';
763                            thumb.appendChild(playIcon);
764                        }
765                    }
766                }
767                
768                thumb.addEventListener('click', () => {
769                    this.lightbox.showItem(index);
770                });
771                
772                return thumb;
773            }
774            
775            updateActive(index) {
776                const thumbnails = this.lightbox.ui.elements.thumbnails.querySelectorAll('.cm-lightbox-thumbn
776ail');
777                thumbnails.forEach((thumb, i) => {
778                    thumb.classList.toggle('active', i === index);
779                });
780                
781                // Scroll active thumbnail into view
782                if (thumbnails[index]) {
783                    thumbnails[index].scrollIntoView({
784                        behavior: 'smooth',
785                        block: 'nearest',
786                        inline: 'center'
787                    });
788                }
789            }
790        }
791        
792        class LightboxInstance {
793            constructor(group) {
794                this.group = group;
795                this.isOpen = false;
796                this.currentIndex = 0;
797                this.currentMember = null;
798                this.currentContent = null;
799                this.isZoomed = false;
800                this.zoomLevel = 1;
801                
802                // Create UI
803                this.ui = new LightboxUI();
804                this.overlay = this.ui.build();
805                
806                // Create components
807                this.viewportCalculator = new ViewportCalculator();
808                this.styleParser = new StyleParser();
809                this.navigationController = new NavigationController(this);
810                this.thumbnailGallery = new ThumbnailGallery(this);
811                
812                // Bind events
813                this.bindEvents();
814                
815                // log('Lightbox instance created for group:', group.name);
816            }
817            
818            bindEvents() {
819                // Close button
820                this.ui.elements.closeButton.addEventListener('click', () => this.close());
821                
822                // Smart click detection for content area
823                this.ui.elements.content.addEventListener('click', (e) => {
824                    // Check if the click was on the image itself
825                    if (e.target.classList.contains('cm-lightbox-image')) {
826                        // Get image dimensions and position
827                        const img = e.target;
828                        const rect = img.getBoundingClientRect();
829                        
830                        // Calculate the actual image dimensions within the container
831                        const naturalAspect = img.naturalWidth / img.naturalHeight;
832                        const containerAspect = rect.width / rect.height;
833                        
834                        let actualWidth, actualHeight, offsetX, offsetY;
835                        
836                        if (naturalAspect > containerAspect) {
837                            // Image is wider - fits to width
838                            actualWidth = rect.width;
839                            actualHeight = rect.width / naturalAspect;
840                            offsetX = 0;
841                            offsetY = (rect.height - actualHeight) / 2;
842                        } else {
843                            // Image is taller - fits to height
844                            actualHeight = rect.height;
845                            actualWidth = rect.height * naturalAspect;
846                            offsetX = (rect.width - actualWidth) / 2;
847                            offsetY = 0;
848                        }
849                        
850                        // Check if click was outside the actual image bounds
851                        const clickX = e.clientX - rect.left;
852                        const clickY = e.clientY - rect.top;
853 
854                        
855                        if (clickX < offsetX || clickX > offsetX + actualWidth ||
856                            clickY < offsetY || clickY > offsetY + actualHeight) {
857                            this.close();
858                        }
859                    } else if (!e.target.closest('.cm-lightbox-image')) {
860                        // Click was on content area but not on image
861                        this.close();
862                    }
863                });
864                
865                // Overlay click to close - close if clicking outside content
866                this.overlay.addEventListener('click', (e) => {
867                    // Close if clicking on overlay or container (but not its children)
868                    if (e.target === this.overlay || e.target === this.ui.elements.container) {
869                        this.close();
870                    }
871                });
872                
873                // Fullscreen button
874                this.ui.elements.fullscreenButton.addEventListener('click', () => this.toggleFullscreen());
875                
876                // Zoom button
877                this.ui.elements.zoomButton.addEventListener('click', () => this.toggleZoom());
878                
879                // Window resize
880                this.resizeHandler = () => this.updateLayout();
881                window.addEventListener('resize', this.resizeHandler);
882            }
883            
884            open(index) {
885                if (this.isOpen) return;
886
887                this.currentIndex = index;
888                this.currentMember = this.group.members[index];
889                this.isOpen = true;
890
891                // log('Opening lightbox for item:', { index: index, member: this.currentMember });
892
893                // Add to body
894                document.body.appendChild(this.overlay);
895
896                // Prevent body scroll
897                document.body.style.overflow = 'hidden';
898
899                // Hide WordPress admin bar if visible (to prevent z-index overlap)
900                const adminBar = document.getElementById('wpadminbar');
901                if (adminBar && window.getComputedStyle(adminBar).display !== 'none') {
902                    this.adminBarWasVisible = true;
903                    adminBar.style.display = 'none';
904                } else {
905                    this.adminBarWasVisible = false;
906                }
907
908                // Show overlay with fade in
909                this.overlay.style.display = 'block';
910                setTimeout(() => {
911                    this.overlay.classList.add('cm-lightbox-visible');
912                }, 10);
913                
914                // Load content
915                this.showItem(index);
916                
917                // Trigger open event
918                this.triggerEvent('lightbox:open', { group: this.group.name, index: index });
919            }
920            
921            close() {
922                if (!this.isOpen) return;
923                
924              // 'Closing lightbox');
925                
926                this.isOpen = false;
927                
928                // Fade out
929                this.overlay.classList.remove('cm-lightbox-visible');
930                
931                setTimeout(() => {
932                    // Remove from DOM
933                    if (this.overlay.parentNode) {
934                        this.overlay.parentNode.removeChild(this.overlay);
935                    }
936
937                    // Restore body scroll
938                    document.body.style.overflow = '';
939
940                    // Restore WordPress admin bar if it was visible
941                    if (this.adminBarWasVisible) {
942                        const adminBar = document.getElementById('wpadminbar');
943                        if (adminBar) {
944                            adminBar.style.display = '';
945                        }
946                    }
947
948                    // Clear content
949                    this.ui.elements.content.innerHTML = '';
950
951                    // Reset zoom
952                    this.isZoomed = false;
953                    this.zoomLevel = 1;
954
955                    // Trigger close event
956                    this.triggerEvent('lightbox:close', { group: this.group.name });
957                }, this.group.options.fadeOutDuration);
958            }
959            
960            showItem(index) {
961                this.currentIndex = index;
962                this.currentMember = this.group.members[index];
963                
964              // 'Showing item:', { index: index, member: this.currentMember });
965                
966                // Clear previous content
967                this.ui.elements.content.innerHTML = '';
968                
969                // Update text
970                this.updateText();
971                
972                // Apply styles
973                const styles = this.styleParser.parse(this.currentMember.attributes.styles || '');
974                const mergedStyles = $.extend({}, this.group.options.styles, styles);
975                this.styleParser.apply(this.overlay, mergedStyles);
976                
977                // Detect content type
978                // log('showItem: Current member URL:', this.currentMember.url);
979                const explicitType = this.currentMember.attributes.type || this.currentMember.attributes.contentType;
980                const contentType = ContentTypeDetector.detect(this.currentMember.url, explicitType);
981               // log('showItem: Detected content type:', contentType);
982                
983                // Show/hide zoom button for images
984                this.ui.elements.zoomButton.style.display = contentType === 'image' ? 'block' : 'none';
985                
986                // Render content
987                ContentRenderer.render(contentType, this.currentMember.url, this.ui.elements.content, this.currentMember.attributes)
988                    .then(content => {
989                        this.currentContent = content;
990                        this.updateLayout();
991                        
992                        // Build thumbnails
993                        this.thumbnailGallery.build();
994                        
995                        // Update navigation visibility
996                        this.updateNavigationVisibility();
997                        
998                        // Trigger loaded event
999                        this.triggerEvent('lightbox:loaded', { 
1000                            group: this.group.name, 
1001                            index: index,
1002                            type: contentType
1003                        });
1004                    })
1005                    .catch(error => {
1006                        log('Error loading content:', error);
1007                        this.ui.elements.content.innerHTML = '<div class="cm-lightbox-error">Error loading content</div>';
1008                    });
1009            }
1010            
1011            updateText() {
1012                const topText = this.currentMember.attributes.textTop || '';
1013                const bottomText = this.currentMember.attributes.textBottom || '';
1014                
1015                // Always keep elements in DOM, just update content
1016                this.ui.elements.topText.innerHTML = topText;
1017                this.ui.elements.bottomText.innerHTML = bottomText;
1018                
1019                // CSS will handle hiding empty elements with :empty pseudo-class
1020            }
1021            
1022            updateLayout() {
1023                if (!this.currentContent) return;
1024                
1025                const contentElement = this.currentContent.element;
1026                
1027                if (this.isZoomed && this.currentContent.type === 'image') {
1028                    // Apply zoom using transform scale
1029                    contentElement.style.transform = `scale(${this.zoomLevel})`;
1030                    contentElement.style.transformOrigin = 'center center';
1031                    
1032                    // Enable scrolling for zoomed content
1033                    this.ui.elements.contentWrapper.style.overflow = 'auto';
1034                } else {
1035                    // Normal view - let CSS handle the sizing
1036                    contentElement.style.transform = '';
1037                    contentElement.style.transformOrigin = '';
1038                    
1039                    // For images, use percentage sizing
1040                    if (this.currentContent.type === 'image') {
1041                        contentElement.style.width = '100%';
1042                        contentElement.style.height = '100%';
1043                        contentElement.style.objectFit = 'contain';
1044                        contentElement.style.maxWidth = 'none';
1045                        contentElement.style.maxHeight = 'none';
1046                    } else if (this.currentContent.type === 'youtube' ||
1047                               this.currentContent.type === 'pdf' ||
1048                               this.currentContent.type === 'maps') {
1049                        // For iframes, use 100% width and height
1050                        contentElement.style.width = '100%';
1051                        contentElement.style.height = '100%';
1052                    } else if (this.currentContent.type === 'audio') {
1053                        // Audio player stays at fixed size
1054                        contentElement.style.width = '400px';
1055                        contentElement.style.height = '100px';
1056                    } else if (this.currentContent.type === 'html') {
1057                        // HTML content fills available space
1058                        contentElement.style.width = '100%';
1059                        contentElement.style.height = '100%';
1060                    }
1061                    
1062                    // Disable scrolling for normal content
1063                    this.ui.elements.contentWrapper.style.overflow = 'hidden';
1064                }
1065            }
1066            
1067            updateNavigationVisibility() {
1068                const hasMultiple = this.group.members.length > 1;
1069                this.ui.elements.prevButton.style.display = hasMultiple ? 'block' : 'none';
1070                this.ui.elements.nextButton.style.display = hasMultiple ? 'block' : 'none';
1071            }
1072            
1073            toggleFullscreen() {
1074                if (!document.fullscreenElement) {
1075                    this.overlay.requestFullscreen().catch(err => {
1076                        log('Error entering fullscreen:', err);
1077                    });
1078                } else {
1079                    document.exitFullscreen();
1080                }
1081            }
1082            
1083            toggleZoom() {
1084                if (this.currentContent.type !== 'image') return;
1085                
1086                this.isZoomed = !this.isZoomed;
1087                
1088                if (this.isZoomed) {
1089                    this.zoomLevel = 2; // 2x zoom
1090                    this.ui.elements.zoomButton.classList.add('active');
1091                } else {
1092                    this.zoomLevel = 1;
1093                    this.ui.elements.zoomButton.classList.remove('active');
1094                }
1095                
1096                this.updateLayout();
1097                
1098                // Setup mouse wheel zoom for images
1099                if (this.isZoomed) {
1100                    this.setupImageZoom();
1101                } else {
1102                    this.teardownImageZoom();
1103                }
1104            }
1105            
1106            setupImageZoom() {
1107                const contentWrapper = this.ui.elements.contentWrapper;
1108                const image = this.currentContent.element;
1109                
1110                // Mouse wheel zoom
1111                this.wheelHandler = (e) => {
1112                    e.preventDefault();
1113                    const delta = e.deltaY > 0 ? -0.1 : 0.1;
1114                    this.zoomLevel = Math.max(1, Math.min(5, this.zoomLevel + delta));
1115                    this.updateLayout();
1116                };
1117                
1118                contentWrapper.addEventListener('wheel', this.wheelHandler);
1119                
1120                // Drag to pan
1121                let isDragging = false;
1122                let startX, startY, scrollLeft, scrollTop;
1123                
1124                this.mouseDownHandler = (e) => {
1125                    isDragging = true;
1126                    contentWrapper.style.cursor = 'grabbing';
1127                    startX = e.pageX - contentWrapper.offsetLeft;
1128                    startY = e.pageY - contentWrapper.offsetTop;
1129                    scrollLeft = contentWrapper.scrollLeft;
1130                    scrollTop = contentWrapper.scrollTop;
1131                };
1132                
1133                this.mouseLeaveHandler = () => {
1134                    isDragging = false;
1135                    contentWrapper.style.cursor = 'grab';
1136                };
1137                
1138                this.mouseUpHandler = () => {
1139                    isDragging = false;
1140                    contentWrapper.style.cursor = 'grab';
1141                };
1142                
1143                this.mouseMoveHandler = (e) => {
1144                    if (!isDragging) return;
1145                    e.preventDefault();
1146                    const x = e.pageX - contentWrapper.offsetLeft;
1147                    const y = e.pageY - contentWrapper.offsetTop;
1148                    const walkX = (x - startX) * 2;
1149                    const walkY = (y - startY) * 2;
1150                    contentWrapper.scrollLeft = scrollLeft - walkX;
1151                    contentWrapper.scrollTop = scrollTop - walkY;
1152                };
1153                
1154                contentWrapper.style.cursor = 'grab';
1155                contentWrapper.addEventListener('mousedown', this.mouseDownHandler);
1156                contentWrapper.addEventListener('mouseleave', this.mouseLeaveHandler);
1157                contentWrapper.addEventListener('mouseup', this.mouseUpHandler);
1158                contentWrapper.addEventListener('mousemove', this.mouseMoveHandler);
1159            }
1160            
1161            teardownImageZoom() {
1162                const contentWrapper = this.ui.elements.contentWrapper;
1163                
1164                if (this.wheelHandler) {
1165                    contentWrapper.removeEventListener('wheel', this.wheelHandler);
1166                }
1167                
1168                if (this.mouseDownHandler) {
1169                    contentWrapper.removeEventListener('mousedown', this.mouseDownHandler);
1170                    contentWrapper.removeEventListener('mouseleave', this.mouseLeaveHandler);
1171                    contentWrapper.removeEventListener('mouseup', this.mouseUpHandler);
1172                    contentWrapper.removeEventListener('mousemove', this.mouseMoveHandler);
1173                }
1174                
1175                contentWrapper.style.cursor = '';
1176            }
1177            
1178            triggerEvent(eventName, detail) {
1179                const event = new CustomEvent(eventName, { detail: detail });
1180                document.dispatchEvent(event);
1181            }
1182            
1183            destroy() {
1184                // Remove event listeners
1185                window.removeEventListener('resize', this.resizeHandler);
1186                this.navigationController.unbindEvents();
1187                
1188                // Close if open
1189                if (this.isOpen) {
1190                    this.close();
1191                }
1192                
1193                // log('Lightbox instance destroyed');
1194            }
1195        }
1196        
1197        // Scanner instance
1198        const scanner = new DOMScanner();
1199        
1200        // Public API implementation
1201        function init(groupName, options) {
1202            if (!groupName) {
1203                log('Error: Group name is required');
1204                return;
1205            }
1206            
1207            // Check if group already exists
1208            if (groups.has(groupName)) {
1209                // log('Group already initialized:', groupName);
1210                return;
1211            }
1212            
1213            // Create group
1214            const group = new Group(groupName, options);
1215            
1216            // Scan DOM for members
1217            const members = scanner.scan(groupName);
1218            members.forEach(member => group.addMember(member));
1219            
1220            // Store group
1221            groups.set(groupName, group);
1222            
1223            // Bind click events
1224            members.forEach((member, index) => {
1225                member.element.addEventListener('click', function(e) {
1226                    e.preventDefault();
1227                    open(groupName, index);
1228                });
1229            });
1230            
1231           // log('Group initialized:', { name: groupName, memberCount: members.length });
1232        }
1233        
1234        function open(groupName, index) {
1235            const group = groups.get(groupName);
1236            if (!group) {
1237                log('Error: Group not found:', groupName);
1238                return;
1239            }
1240            
1241            if (index < 0 || index >= group.members.length) {
1242                log('Error: Invalid index:', index);
1243                return;
1244            }
1245            
1246            // Close any existing lightbox
1247            if (activeInstance) {
1248                activeInstance.close();
1249            }
1250            
1251            // Create new instance
1252            activeInstance = new LightboxInstance(group);
1253            activeInstance.open(index);
1254        }
1255        
1256        function close() {
1257            if (activeInstance) {
1258                activeInstance.close();
1259                activeInstance = null;
1260            }
1261        }
1262        
1263        function navigate(direction) {
1264            if (activeInstance && activeInstance.navigationController) {
1265                activeInstance.navigationController.navigate(direction);
1266            }
1267        }
1268        
1269        function hasGroup(groupName) {
1270            return groups.has(groupName);
1271        }
1272        
1273        function destroy(groupName) {
1274            if (groupName) {
1275                // Destroy specific group
1276                const group = groups.get(groupName);
1277                if (group) {
1278                    // Remove click events
1279                    group.members.forEach(member => {
1280                        const newElement = member.element.cloneNode(true);
1281                        member.element.parentNode.replaceChild(newElement, member.element);
1282                    });
1283                    
1284                    groups.delete(groupName);
1285                    // log('Group destroyed:', groupName);
1286                }
1287            } else {
1288                // Destroy all groups
1289                groups.forEach((group, name) => {
1290                    destroy(name);
1291                });
1292                
1293                // Close active instance
1294                if (activeInstance) {
1295                    activeInstance.destroy();
1296                    activeInstance = null;
1297                }
1298            }
1299        }
1300        
1301        // Public API
1302        return {
1303            init: init,
1304            open: open,
1305            close: close,
1306            navigate: navigate,
1307            hasGroup: hasGroup,
1308            destroy: destroy,
1309            version: '1.0.0'
1310        };
1311        
1312    })();
1313    
1314    // Auto-initialize on DOM ready
1315    $(document).ready(function() {
1316        // Look for groups in the DOM
1317        const foundGroups = new Set();
1318
1319        $('[data-cm-lb-group]').each(function() {
1320            const groupName = $(this).data('cm-lb-group');
1321            if (groupName && !foundGroups.has(groupName)) {
1322                foundGroups.add(groupName);
1323
1324                // Get default attributes from first element
1325                const defaultAttrs = {};
1326                const $this = $(this);
1327
1328                $.each(this.attributes, function() {
1329                    if (this.name.startsWith('data-cm-lb-') &&
1330                        this.name !== 'data-cm-lb-group' &&
1331                        this.name !== 'data-cm-lb-url') {
1332                        const key = this.name.replace('data-cm-lb-', '');
1333                        defaultAttrs[key] = this.value;
1334                    }
1335                });
1336
1337                // Initialize group
1338                CMLightbox.init(groupName, defaultAttrs);
1339            }
1340        });
1341
1342        if (foundGroups.size > 0) {
1343           // console.log('[CM-Lightbox] Auto-initialized ' + foundGroups.size + ' groups');
1344        }
1345    });
1346
1347    // Listen for dynamic content events (pagination, AJAX loads, etc.)
1348    // This allows any plugin to trigger lightbox reinitialization by emitting custom events
1349    $(document).on('cmTilesLoaded', function(event, $newContent) {
1350        // Reinitialize lightbox for newly loaded content
1351        const processedGroups = new Set();
1352
1353        // Check if specific content was provided, otherwise scan entire document
1354        const $context = $newContent && $newContent.length ? $newContent : $(document);
1355
1356        $context.find('[data-cm-lb-group]').each(function() {
1357            const groupName = $(this).data('cm-lb-group');
1358
1359            if (groupName && !processedGroups.has(groupName)) {
1360                processedGroups.add(groupName);
1361
1362                // Get default attributes from first element
1363                const defaultAttrs = {};
1364                $.each(this.attributes, function() {
1365                    if (this.name.startsWith('data-cm-lb-') &&
1366                        this.name !== 'data-cm-lb-group' &&
1367                        this.name !== 'data-cm-lb-url') {
1368                        const key = this.name.replace('data-cm-lb-', '');
1369                        defaultAttrs[key] = this.value;
1370                    }
1371                });
1372
1373                // If group already exists, destroy it first
1374                if (CMLightbox.hasGroup(groupName)) {
1375                    CMLightbox.destroy(groupName);
1376                }
1377
1378                // Initialize (or reinitialize) the group
1379                CMLightbox.init(groupName, defaultAttrs);
1380            }
1381        });
1382
1383        if (processedGroups.size > 0) {
1384            console.log('[CM-Lightbox] Reinitialized ' + processedGroups.size + ' group(s) after dynamic content load');
1385        }
1386    });
1387
1388})(jQuery, window, document);

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.