zoomora 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/zoomora.js ADDED
@@ -0,0 +1,1382 @@
1
+ /*!
2
+ * Zoomora Lightbox Plugin v1.0.0
3
+ * A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
4
+ *
5
+ * Copyright (c) 2025 Faruk Ahmed (FrontTheme)
6
+ * Licensed under MIT License
7
+ */
8
+
9
+ // Import styles
10
+ import './styles/zoomora.scss';
11
+
12
+ (function (global, factory) {
13
+ 'use strict';
14
+
15
+ if (typeof module === 'object' && typeof module.exports === 'object') {
16
+ // CommonJS/Node.js
17
+ module.exports = factory();
18
+ } else if (typeof define === 'function' && define.amd) {
19
+ // AMD
20
+ define(factory);
21
+ } else {
22
+ // Browser globals
23
+ global.Zoomora = factory();
24
+ }
25
+ }(typeof window !== 'undefined' ? window : this, function () {
26
+ 'use strict';
27
+
28
+ /**
29
+ * Zoomora Lightbox Plugin
30
+ * @class
31
+ */
32
+ class Zoomora {
33
+ /**
34
+ * Default configuration options
35
+ * @type {Object}
36
+ */
37
+ static get defaults() {
38
+ return {
39
+ selector: '[data-zoomora]',
40
+ showCounter: true,
41
+ showThumbnails: true,
42
+ showFullscreen: true,
43
+ showZoom: true,
44
+ transition: 'fade', // 'fade' or 'slide'
45
+ useHref: false, // Use href instead of data-src
46
+ maxZoomScale: 3, // Maximum zoom scale
47
+ zoomStep: 0.1, // Zoom step for scroll wheel
48
+ animationDuration: 300, // Animation duration in ms
49
+ onOpen: null, // Callback when lightbox opens
50
+ onClose: null, // Callback when lightbox closes
51
+ onNext: null, // Callback when navigating to next item
52
+ onPrev: null, // Callback when navigating to previous item
53
+
54
+ // Auto-hide controls
55
+ showAutoHideToggle: true, // Show auto-hide toggle button
56
+ autoHideDelay: 3000, // Delay in ms before hiding controls
57
+ autoHideEnabled: false, // Default state of auto-hide
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Constructor
63
+ * @param {Object} options - Configuration options
64
+ */
65
+ constructor(options = {}) {
66
+ this.options = {...Zoomora.defaults, ...options};
67
+ this.currentIndex = 0;
68
+ this.items = [];
69
+ this.isZoomed = false;
70
+ this.isFullscreen = false;
71
+ this.isDragging = false;
72
+ this.thumbnailsVisible = true;
73
+ this.startX = 0;
74
+ this.startY = 0;
75
+ this.currentX = 0;
76
+ this.currentY = 0;
77
+ this.currentScale = 1;
78
+ this.isInitialized = false;
79
+ this.eventListeners = new Map();
80
+
81
+ // Auto-hide controls
82
+ this.autoHideEnabled = false;
83
+ this.autoHideTimeout = null;
84
+ this.autoHideDelay = 3000;
85
+ this.isControlsVisible = true;
86
+ this.idleTimer = null;
87
+
88
+ // Bound methods for event listeners
89
+ this.boundMethods = {
90
+ handleDocumentClick: this.handleDocumentClick.bind(this),
91
+ handleKeydown: this.handleKeydown.bind(this),
92
+ handleFullscreenChange: this.handleFullscreenChange.bind(this),
93
+ handleZoomoraClick: this.handleZoomoraClick.bind(this),
94
+ handleContentClick: this.handleContentClick.bind(this),
95
+ handleMouseDown: this.handleMouseDown.bind(this),
96
+ handleMouseMove: this.handleMouseMove.bind(this),
97
+ handleMouseUp: this.handleMouseUp.bind(this),
98
+ handleTouchStart: this.handleTouchStart.bind(this),
99
+ handleTouchMove: this.handleTouchMove.bind(this),
100
+ handleTouchEnd: this.handleTouchEnd.bind(this),
101
+ };
102
+
103
+ this.init();
104
+ }
105
+
106
+ /**
107
+ * Initialize the plugin
108
+ */
109
+ init() {
110
+ if (this.isInitialized) {
111
+ console.warn('Zoomora: Plugin already initialized');
112
+ return;
113
+ }
114
+
115
+ try {
116
+ this.createLightbox();
117
+ this.bindEvents();
118
+
119
+ this.isInitialized = true;
120
+ } catch (error) {
121
+ console.error('Zoomora: Initialization failed', error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Static method to bind to anchor tags with href
127
+ * @param {string} selector - CSS selector
128
+ * @param {Object} options - Configuration options
129
+ * @returns {Zoomora} - New Zoomora instance
130
+ */
131
+ static bind(selector, options = {}) {
132
+ const enhancedOptions = {
133
+ ...options,
134
+ selector: selector,
135
+ useHref: true
136
+ };
137
+ return new Zoomora(enhancedOptions);
138
+ }
139
+
140
+ /**
141
+ * Create lightbox HTML structure
142
+ */
143
+ createLightbox() {
144
+ // Check if lightbox already exists
145
+ if (document.getElementById('zoomoraLightbox')) {
146
+ console.warn('Zoomora: Lightbox already exists in DOM');
147
+ return;
148
+ }
149
+
150
+ const zoomoraHTML = `
151
+ <div class="zoomora" id="zoomoraLightbox">
152
+ <div class="zoomora-container">
153
+ <div class="zoomora-header" id="zoomoraHeader">
154
+ <div class="zoomora-counter" id="zoomoraCounter">1 / 1</div>
155
+ <div class="zoomora-controls">
156
+ <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
157
+ <i class="zoomorai-thumbnails"></i>
158
+ </button>
159
+ <button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
160
+ <i class="zoomorai-controls-off"></i>
161
+ </button>
162
+ <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
163
+ <i class="zoomorai-zoom"></i>
164
+ </button>
165
+ <button class="zoomora-btn" id="zoomoraFullscreenBtn" title="Fullscreen">
166
+ <i class="zoomorai-expand"></i>
167
+ </button>
168
+ <button class="zoomora-btn" id="zoomoraCollapseBtn" title="Exit Fullscreen" style="display: none;">
169
+ <i class="zoomorai-collapse"></i>
170
+ </button>
171
+ <button class="zoomora-btn" id="zoomoraCloseBtn" title="Close">
172
+ <i class="zoomorai-close"></i>
173
+ </button>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="zoomora-content" id="zoomoraContent">
178
+ <div class="zoomora-loading" id="zoomoraLoading"></div>
179
+ <div class="zoomora-slides-container" id="zoomoraSlidesContainer"></div>
180
+ </div>
181
+
182
+ <button class="zoomora-nav prev" id="zoomoraPrevBtn">
183
+ <i class="zoomorai-arrow-left"></i>
184
+ </button>
185
+ <button class="zoomora-nav next" id="zoomoraNextBtn">
186
+ <i class="zoomorai-arrow-right"></i>
187
+ </button>
188
+
189
+ <div class="zoomora-caption" id="zoomoraCaption"></div>
190
+ <div class="zoomora-thumbnails" id="zoomoraThumbs"></div>
191
+ </div>
192
+ </div>
193
+ `;
194
+
195
+ document.body.insertAdjacentHTML('beforeend', zoomoraHTML);
196
+
197
+ // Cache DOM elements
198
+ this.zoomora = document.getElementById('zoomoraLightbox');
199
+ this.header = document.getElementById('zoomoraHeader');
200
+ this.content = document.getElementById('zoomoraContent');
201
+ this.slidesContainer = document.getElementById('zoomoraSlidesContainer');
202
+ this.counter = document.getElementById('zoomoraCounter');
203
+ this.thumbs = document.getElementById('zoomoraThumbs');
204
+ this.loading = document.getElementById('zoomoraLoading');
205
+ this.caption = document.getElementById('zoomoraCaption');
206
+ this.prevBtn = document.getElementById('zoomoraPrevBtn');
207
+ this.nextBtn = document.getElementById('zoomoraNextBtn');
208
+
209
+ // Verify all elements were created
210
+ if (!this.zoomora || !this.content || !this.counter || !this.thumbs || !this.loading || !this.caption) {
211
+ throw new Error('Failed to create required DOM elements');
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Bind event listeners
217
+ */
218
+ bindEvents() {
219
+ // Document click handler for opening lightbox
220
+ this.addEventListener(document, 'click', this.boundMethods.handleDocumentClick);
221
+
222
+ // Control button handlers
223
+ this.addEventListener(document.getElementById('zoomoraCloseBtn'), 'click', () => this.close());
224
+ this.addEventListener(document.getElementById('zoomoraPrevBtn'), 'click', () => this.prev());
225
+ this.addEventListener(document.getElementById('zoomoraNextBtn'), 'click', () => this.next());
226
+ this.addEventListener(document.getElementById('zoomoraZoomBtn'), 'click', () => this.toggleZoom());
227
+ this.addEventListener(document.getElementById('zoomoraFullscreenBtn'), 'click', () => this.toggleFullscreen());
228
+ this.addEventListener(document.getElementById('zoomoraCollapseBtn'), 'click', () => this.exitFullscreen());
229
+ this.addEventListener(document.getElementById('zoomoraThumbnailBtn'), 'click', () => this.toggleThumbnails());
230
+
231
+ // Fullscreen change events
232
+ this.addEventListener(document, 'fullscreenchange', this.boundMethods.handleFullscreenChange);
233
+ this.addEventListener(document, 'webkitfullscreenchange', this.boundMethods.handleFullscreenChange);
234
+ this.addEventListener(document, 'mozfullscreenchange', this.boundMethods.handleFullscreenChange);
235
+ this.addEventListener(document, 'MSFullscreenChange', this.boundMethods.handleFullscreenChange);
236
+
237
+ // Keyboard navigation
238
+ this.addEventListener(document, 'keydown', this.boundMethods.handleKeydown);
239
+
240
+ // Lightbox click to close
241
+ this.addEventListener(this.zoomora, 'click', this.boundMethods.handleZoomoraClick);
242
+
243
+ // Content click for zoom toggle
244
+ this.addEventListener(this.content, 'click', this.boundMethods.handleContentClick);
245
+
246
+ // Mouse drag events
247
+ this.addEventListener(this.content, 'mousedown', this.boundMethods.handleMouseDown);
248
+ this.addEventListener(this.content, 'mousemove', this.boundMethods.handleMouseMove);
249
+ this.addEventListener(this.content, 'mouseup', this.boundMethods.handleMouseUp);
250
+ this.addEventListener(this.content, 'mouseleave', this.boundMethods.handleMouseUp);
251
+
252
+ // Touch events
253
+ this.addEventListener(this.content, 'touchstart', this.boundMethods.handleTouchStart, {passive: false});
254
+ this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
255
+ this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
256
+
257
+ // Add auto-hide toggle button handler
258
+ this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
259
+
260
+ // Add mouse/touch activity listeners for auto-hide
261
+ this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
262
+ this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
263
+ this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
264
+ }
265
+
266
+ /**
267
+ * Add event listener with cleanup tracking
268
+ * @param {Element} element - DOM element
269
+ * @param {string} event - Event type
270
+ * @param {Function} handler - Event handler
271
+ * @param {Object} options - Event options
272
+ */
273
+ addEventListener(element, event, handler, options = {}) {
274
+ if (!element) return;
275
+
276
+ element.addEventListener(event, handler, options);
277
+
278
+ // Track for cleanup
279
+ const key = `${element.constructor.name}_${event}`;
280
+ if (!this.eventListeners.has(key)) {
281
+ this.eventListeners.set(key, []);
282
+ }
283
+ this.eventListeners.get(key).push({element, event, handler, options});
284
+ }
285
+
286
+ /**
287
+ * Handle document click events
288
+ * @param {Event} e - Click event
289
+ */
290
+ handleDocumentClick(e) {
291
+ const target = e.target.closest(this.options.selector);
292
+ if (target) {
293
+ e.preventDefault();
294
+ this.open(target);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Handle keyboard events
300
+ * @param {KeyboardEvent} e - Keyboard event
301
+ */
302
+ handleKeydown(e) {
303
+ if (!this.zoomora.classList.contains('active')) return;
304
+
305
+ switch (e.key) {
306
+ case 'Escape':
307
+ if (this.isFullscreen) {
308
+ this.exitFullscreen();
309
+ } else {
310
+ this.close();
311
+ }
312
+ break;
313
+ case 'ArrowLeft':
314
+ this.prev();
315
+ break;
316
+ case 'ArrowRight':
317
+ this.next();
318
+ break;
319
+ case 'z':
320
+ case 'Z':
321
+ this.toggleZoom();
322
+ break;
323
+ case 'f':
324
+ case 'F':
325
+ this.toggleFullscreen();
326
+ break;
327
+ case 't':
328
+ case 'T':
329
+ this.toggleThumbnails();
330
+ break;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Handle fullscreen change events
336
+ */
337
+ handleFullscreenChange() {
338
+ const isInFullscreen = document.fullscreenElement ||
339
+ document.webkitFullscreenElement ||
340
+ document.mozFullScreenElement ||
341
+ document.msFullscreenElement;
342
+
343
+ if (!isInFullscreen && this.isFullscreen) {
344
+ this.isFullscreen = false;
345
+ this.zoomora.classList.remove('fullscreen');
346
+ this.updateButtons();
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Handle zoomora container clicks
352
+ * @param {Event} e - Click event
353
+ */
354
+ handleZoomoraClick(e) {
355
+ if (e.target === this.zoomora) {
356
+ this.close();
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Handle content clicks
362
+ * @param {Event} e - Click event
363
+ */
364
+ handleContentClick(e) {
365
+ const media = e.target.closest('.zoomora-media');
366
+ if (media && !media.classList.contains('no-zoom') && !this.isDragging) {
367
+ this.toggleZoom();
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Handle mouse down events
373
+ * @param {MouseEvent} e - Mouse event
374
+ */
375
+ handleMouseDown(e) {
376
+ this.startDrag(e);
377
+ }
378
+
379
+ /**
380
+ * Handle mouse move events
381
+ * @param {MouseEvent} e - Mouse event
382
+ */
383
+ handleMouseMove(e) {
384
+ this.drag(e);
385
+ }
386
+
387
+ /**
388
+ * Handle mouse up events
389
+ */
390
+ handleMouseUp() {
391
+ this.endDrag();
392
+ }
393
+
394
+ /**
395
+ * Handle touch start events
396
+ * @param {TouchEvent} e - Touch event
397
+ */
398
+ handleTouchStart(e) {
399
+ this.startDrag(e);
400
+ }
401
+
402
+ /**
403
+ * Handle touch move events
404
+ * @param {TouchEvent} e - Touch event
405
+ */
406
+ handleTouchMove(e) {
407
+ this.drag(e);
408
+ }
409
+
410
+ /**
411
+ * Handle touch end events
412
+ */
413
+ handleTouchEnd() {
414
+ this.endDrag();
415
+ }
416
+
417
+ /**
418
+ * Get image source from element
419
+ * @param {Element} item - DOM element
420
+ * @returns {string} - Image source URL
421
+ */
422
+ getImageSource(item) {
423
+ const src = item.dataset.src || item.src || item.getAttribute('src');
424
+ const type = item.dataset.type || 'image';
425
+
426
+ // Handle YouTube URLs for video types
427
+ if (type === 'video' && src) {
428
+ return this.convertToEmbedUrl(src);
429
+ }
430
+
431
+ return src;
432
+ }
433
+
434
+ /**
435
+ * Convert YouTube URLs to embed format
436
+ * @param url
437
+ * @returns {*|string}
438
+ */
439
+ convertToEmbedUrl(url) {
440
+ // If already an embed URL, return as is
441
+ if (url.includes('youtube.com/embed/')) {
442
+ return url;
443
+ }
444
+
445
+ // Handle different YouTube URL formats
446
+ let videoId = null;
447
+ let startTime = null;
448
+
449
+ // Extract video ID and start time from different YouTube URL formats
450
+ if (url.includes('youtube.com/watch')) {
451
+ const urlParams = new URLSearchParams(url.split('?')[1]);
452
+ videoId = urlParams.get('v');
453
+ const t = urlParams.get('t');
454
+ if (t) {
455
+ // Convert time format (e.g., "111s" to seconds)
456
+ startTime = t.replace('s', '');
457
+ }
458
+ } else if (url.includes('youtu.be/')) {
459
+ const parts = url.split('/');
460
+ const videoAndParams = parts[parts.length - 1];
461
+ if (videoAndParams.includes('?')) {
462
+ videoId = videoAndParams.split('?')[0];
463
+ const urlParams = new URLSearchParams(videoAndParams.split('?')[1]);
464
+ const t = urlParams.get('t');
465
+ if (t) startTime = t.replace('s', '');
466
+ } else {
467
+ videoId = videoAndParams;
468
+ }
469
+ }
470
+
471
+ if (videoId) {
472
+ let embedUrl = `https://www.youtube.com/embed/${videoId}`;
473
+ const params = new URLSearchParams();
474
+
475
+ if (startTime) {
476
+ params.append('start', startTime);
477
+ }
478
+
479
+ // Add other useful parameters
480
+ params.append('rel', '0'); // Don't show related videos
481
+ params.append('modestbranding', '1'); // Modest branding
482
+
483
+ // Option: Add autoplay for YouTube videos (uncomment if desired)
484
+ // params.append('autoplay', '1');
485
+
486
+ if (params.toString()) {
487
+ embedUrl += '?' + params.toString();
488
+ }
489
+
490
+ return embedUrl;
491
+ }
492
+
493
+ // If not a YouTube URL or couldn't parse, return original
494
+ return url;
495
+ }
496
+
497
+ /**
498
+ * Get thumbnail source from element
499
+ * @param {Element} item - DOM element
500
+ * @returns {string} - Thumbnail source URL
501
+ */
502
+ getThumbnailSource(item) {
503
+ const type = item.dataset.type || 'image';
504
+
505
+ // For video items, always use the img.src as thumbnail (poster image)
506
+ if (type === 'video') {
507
+ return item.src || item.getAttribute('src');
508
+ }
509
+
510
+ // For regular images, use data-src or fallback to src
511
+ return item.dataset.src || item.src || item.getAttribute('src');
512
+ }
513
+
514
+ /**
515
+ * Get group identifier from element
516
+ * @param {Element} element - DOM element
517
+ * @returns {string} - Group identifier
518
+ */
519
+ getGroup(element) {
520
+ if (this.options.useHref) {
521
+ return element.dataset.lightbox || element.dataset.gallery || 'default';
522
+ }
523
+ return element.dataset.zoomora || 'default';
524
+ }
525
+
526
+ /**
527
+ * Open lightbox with specified element
528
+ * @param {Element} element - DOM element to open
529
+ */
530
+ open(element) {
531
+ if (!element) return;
532
+
533
+ const group = this.getGroup(element);
534
+ let groupSelector;
535
+
536
+ if (this.options.useHref) {
537
+ if (element.dataset.lightbox) {
538
+ groupSelector = `[data-lightbox="${group}"]`;
539
+ } else if (element.dataset.gallery) {
540
+ groupSelector = `[data-gallery="${group}"]`;
541
+ } else {
542
+ groupSelector = this.options.selector;
543
+ }
544
+ } else {
545
+ groupSelector = `[data-zoomora="${group}"]`;
546
+ }
547
+
548
+ this.items = Array.from(document.querySelectorAll(groupSelector));
549
+ this.currentIndex = Math.max(0, this.items.indexOf(element));
550
+
551
+ // Reset states
552
+ this.isZoomed = false;
553
+ this.isFullscreen = false;
554
+ this.thumbnailsVisible = true;
555
+ this.currentX = 0;
556
+ this.currentY = 0;
557
+ this.currentScale = 1;
558
+
559
+ this.zoomora.classList.add('active');
560
+ document.body.style.overflow = 'hidden';
561
+
562
+ this.loadContent();
563
+ this.updateThumbnails();
564
+ this.updateCounter();
565
+ this.updateNavigation();
566
+ this.updateCaption();
567
+ this.updateButtons();
568
+
569
+ // Initialize auto-hide if enabled
570
+ if (this.autoHideEnabled) {
571
+ this.startAutoHideTimer();
572
+ } else {
573
+ this.showControls();
574
+ }
575
+
576
+ // Trigger callback
577
+ if (typeof this.options.onOpen === 'function') {
578
+ this.options.onOpen(element, this.currentIndex);
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Close lightbox
584
+ */
585
+ close() {
586
+ if (this.isFullscreen) {
587
+ this.exitFullscreen();
588
+ }
589
+
590
+ this.zoomora.classList.remove('active');
591
+ document.body.style.overflow = '';
592
+ this.isZoomed = false;
593
+ this.isFullscreen = false;
594
+ this.currentX = 0;
595
+ this.currentY = 0;
596
+ this.currentScale = 1;
597
+
598
+ const media = this.content.querySelector('.zoomora-media');
599
+ if (media) {
600
+ media.classList.remove('zoomed');
601
+ media.style.transform = '';
602
+ media.style.cursor = '';
603
+ }
604
+
605
+ this.zoomora.classList.remove('fullscreen');
606
+ this.updateButtons();
607
+
608
+ // Clean up auto-hide
609
+ this.stopAutoHideTimer();
610
+ this.showControls();
611
+
612
+ // Trigger callback
613
+ if (typeof this.options.onClose === 'function') {
614
+ this.options.onClose(this.currentIndex);
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Navigate to previous item
620
+ */
621
+ prev() {
622
+ if (this.currentIndex > 0) {
623
+ this.currentIndex--;
624
+ this.loadContent();
625
+ this.updateCounter();
626
+ this.updateThumbnails();
627
+ this.updateNavigation();
628
+ this.updateCaption();
629
+
630
+ // Trigger callback
631
+ if (typeof this.options.onPrev === 'function') {
632
+ this.options.onPrev(this.currentIndex);
633
+ }
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Navigate to next item
639
+ */
640
+ next() {
641
+ if (this.currentIndex < this.items.length - 1) {
642
+ this.currentIndex++;
643
+ this.loadContent();
644
+ this.updateCounter();
645
+ this.updateThumbnails();
646
+ this.updateNavigation();
647
+ this.updateCaption();
648
+
649
+ // Trigger callback
650
+ if (typeof this.options.onNext === 'function') {
651
+ this.options.onNext(this.currentIndex);
652
+ }
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Load content for current item
658
+ */
659
+ loadContent() {
660
+ if (!this.items[this.currentIndex]) return;
661
+
662
+ const item = this.items[this.currentIndex];
663
+ const src = this.getImageSource(item);
664
+ const type = item.dataset.type || 'image';
665
+
666
+ if (!src) {
667
+ console.error('Zoomora: No source found for item');
668
+ return;
669
+ }
670
+
671
+ // Show loading spinner
672
+ this.loading.style.display = 'block';
673
+
674
+ // Clear content but keep the loading reference intact
675
+ // Don't replace innerHTML, just clear the slides
676
+ if (this.slidesContainer) {
677
+ this.slidesContainer.innerHTML = '';
678
+ }
679
+
680
+ // Add slight delay for smooth transition
681
+ setTimeout(() => {
682
+ this.renderContent(src, type, item);
683
+ }, 100);
684
+ }
685
+
686
+ /**
687
+ * Render content based on type
688
+ * @param {string} src - Source URL
689
+ * @param {string} type - Content type
690
+ * @param {Element} item - Original item element
691
+ */
692
+ renderContent(src, type, item) {
693
+ const slideContainer = document.createElement('div');
694
+ slideContainer.className = 'zoomora-slide-container';
695
+
696
+ // Create slide wrapper
697
+ const slide = document.createElement('div');
698
+ slide.className = `zoomora-slide ${this.options.transition === 'slide' ? 'slide-effect' : 'fade-effect'}`;
699
+ slide.style.opacity = '0';
700
+ slide.style.transform = this.options.transition === 'slide' ? 'translateX(100%)' : 'scale(0.8)';
701
+
702
+ let mediaElement;
703
+
704
+ try {
705
+ switch (type) {
706
+ case 'video':
707
+ if (src.endsWith('.mp4') || src.endsWith('.webm') || src.endsWith('.ogg')) {
708
+ const videoContainer = document.createElement('div');
709
+ videoContainer.className = 'zoomora-video-container';
710
+
711
+ mediaElement = document.createElement('video');
712
+ mediaElement.src = src;
713
+ mediaElement.className = 'zoomora-video';
714
+ mediaElement.setAttribute('preload', 'metadata');
715
+
716
+ const posterSrc = item.src || item.getAttribute('src');
717
+ if (posterSrc) {
718
+ mediaElement.setAttribute('poster', posterSrc);
719
+ }
720
+
721
+ const playButton = document.createElement('div');
722
+ playButton.className = 'zoomora-play-button';
723
+ playButton.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
724
+
725
+ playButton.addEventListener('click', () => {
726
+ mediaElement.play();
727
+ playButton.style.display = 'none';
728
+ mediaElement.setAttribute('controls', '');
729
+ });
730
+
731
+ mediaElement.addEventListener('ended', () => {
732
+ playButton.style.display = 'flex';
733
+ });
734
+
735
+ videoContainer.appendChild(mediaElement);
736
+ videoContainer.appendChild(playButton);
737
+ slideContainer.appendChild(videoContainer);
738
+ } else {
739
+ mediaElement = document.createElement('iframe');
740
+ mediaElement.src = src;
741
+ mediaElement.className = 'zoomora-video';
742
+ mediaElement.setAttribute('allowfullscreen', '');
743
+ mediaElement.setAttribute('frameborder', '0');
744
+ slideContainer.appendChild(mediaElement);
745
+ }
746
+ break;
747
+
748
+ case 'iframe':
749
+ mediaElement = document.createElement('iframe');
750
+ mediaElement.src = src;
751
+ mediaElement.className = 'zoomora-iframe';
752
+ mediaElement.setAttribute('frameborder', '0');
753
+ slideContainer.appendChild(mediaElement);
754
+ break;
755
+
756
+ default:
757
+ mediaElement = document.createElement('img');
758
+ mediaElement.src = src;
759
+ mediaElement.className = 'zoomora-media';
760
+ mediaElement.alt = item.alt || item.title || '';
761
+
762
+ mediaElement.onload = () => {
763
+ this.loading.style.display = 'none';
764
+ this.checkZoomability(mediaElement);
765
+ this.animateSlideIn(slide);
766
+ };
767
+
768
+ mediaElement.onerror = () => {
769
+ this.loading.style.display = 'none';
770
+ console.error('Zoomora: Failed to load image:', src);
771
+ this.animateSlideIn(slide);
772
+ };
773
+
774
+ slideContainer.appendChild(mediaElement);
775
+ }
776
+
777
+ slide.appendChild(slideContainer);
778
+
779
+ // Clear existing slides and add new one
780
+ this.slidesContainer.innerHTML = '';
781
+ this.slidesContainer.appendChild(slide);
782
+
783
+ if (type !== 'image') {
784
+ this.loading.style.display = 'none';
785
+ this.animateSlideIn(slide);
786
+ }
787
+
788
+ } catch (error) {
789
+ console.error('Zoomora: Error rendering content:', error);
790
+ this.loading.style.display = 'none';
791
+ }
792
+ }
793
+
794
+ // Animation helper
795
+ animateSlideIn(slide) {
796
+ // Force reflow
797
+ slide.offsetHeight;
798
+
799
+ // Add active class and animate
800
+ slide.classList.add('active');
801
+
802
+ if (this.options.transition === 'slide') {
803
+ slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
804
+ slide.style.transform = 'translateX(0)';
805
+ slide.style.opacity = '1';
806
+ } else {
807
+ slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
808
+ slide.style.transform = 'scale(1)';
809
+ slide.style.opacity = '1';
810
+ }
811
+ }
812
+
813
+ // Toggle auto-hide controls
814
+ toggleAutoHide() {
815
+ this.autoHideEnabled = !this.autoHideEnabled;
816
+ const autoHideBtn = document.getElementById('zoomoraAutoHideBtn');
817
+
818
+ if (this.autoHideEnabled) {
819
+ autoHideBtn.classList.add('active');
820
+ autoHideBtn.title = 'Disable Auto-Hide Controls';
821
+ this.startAutoHideTimer();
822
+ } else {
823
+ autoHideBtn.classList.remove('active');
824
+ autoHideBtn.title = 'Enable Auto-Hide Controls';
825
+ this.stopAutoHideTimer();
826
+ this.showControls();
827
+ }
828
+ }
829
+
830
+ // Handle user activity
831
+ handleUserActivity() {
832
+ if (!this.autoHideEnabled) return;
833
+
834
+ this.showControls();
835
+ this.startAutoHideTimer();
836
+ }
837
+
838
+ // Start auto-hide timer
839
+ startAutoHideTimer() {
840
+ this.stopAutoHideTimer();
841
+
842
+ this.autoHideTimeout = setTimeout(() => {
843
+ this.hideControls();
844
+ }, this.options.autoHideDelay);
845
+ }
846
+
847
+ // Stop auto-hide timer
848
+ stopAutoHideTimer() {
849
+ if (this.autoHideTimeout) {
850
+ clearTimeout(this.autoHideTimeout);
851
+ this.autoHideTimeout = null;
852
+ }
853
+ }
854
+
855
+ // Show controls
856
+ showControls() {
857
+ if (this.isControlsVisible) return;
858
+
859
+ this.isControlsVisible = true;
860
+ this.zoomora.classList.remove('controls-hidden');
861
+
862
+ // Animate controls back in
863
+ if (this.header) {
864
+ this.header.style.transform = 'translateY(0)';
865
+ this.header.style.opacity = '1';
866
+ }
867
+
868
+ if (this.caption) {
869
+ this.caption.style.transform = 'translateX(-50%) translateY(0)';
870
+ this.caption.style.opacity = '1';
871
+ }
872
+
873
+ if (this.thumbs) {
874
+ this.thumbs.style.transform = 'translateX(-50%) translateY(0)';
875
+ this.thumbs.style.opacity = '1';
876
+ }
877
+
878
+ if (this.prevBtn) {
879
+ this.prevBtn.style.transform = 'translateX(0)';
880
+ this.prevBtn.style.opacity = '1';
881
+ }
882
+
883
+ if (this.nextBtn) {
884
+ this.nextBtn.style.transform = 'translateX(0)';
885
+ this.nextBtn.style.opacity = '1';
886
+ }
887
+ }
888
+
889
+ // Hide controls
890
+ hideControls() {
891
+ if (!this.isControlsVisible) return;
892
+
893
+ this.isControlsVisible = false;
894
+ this.zoomora.classList.add('controls-hidden');
895
+
896
+ // Animate controls out
897
+ if (this.header) {
898
+ this.header.style.transform = 'translateY(-100%)';
899
+ this.header.style.opacity = '0';
900
+ }
901
+
902
+ if (this.caption) {
903
+ this.caption.style.transform = 'translateX(-50%) translateY(100%)';
904
+ this.caption.style.opacity = '0';
905
+ }
906
+
907
+ if (this.thumbs) {
908
+ this.thumbs.style.transform = 'translateX(-50%) translateY(100%)';
909
+ this.thumbs.style.opacity = '0';
910
+ }
911
+
912
+ if (this.prevBtn) {
913
+ this.prevBtn.style.transform = 'translateX(-100%)';
914
+ this.prevBtn.style.opacity = '0';
915
+ }
916
+
917
+ if (this.nextBtn) {
918
+ this.nextBtn.style.transform = 'translateX(100%)';
919
+ this.nextBtn.style.opacity = '0';
920
+ }
921
+ }
922
+
923
+ /**
924
+ * Check if image can be zoomed
925
+ * @param {HTMLImageElement} img - Image element
926
+ */
927
+ checkZoomability(img) {
928
+ if (!img || !img.naturalWidth || !img.naturalHeight) return;
929
+
930
+ const imgRect = img.getBoundingClientRect();
931
+ const renderedWidth = imgRect.width;
932
+ const renderedHeight = imgRect.height;
933
+ const naturalWidth = img.naturalWidth;
934
+ const naturalHeight = img.naturalHeight;
935
+
936
+ const isScaledDown = naturalWidth > renderedWidth || naturalHeight > renderedHeight;
937
+ const zoomRatioX = naturalWidth / renderedWidth;
938
+ const zoomRatioY = naturalHeight / renderedHeight;
939
+ const maxZoomRatio = Math.max(zoomRatioX, zoomRatioY);
940
+
941
+ // Store zoom info
942
+ img.dataset.zoomRatio = maxZoomRatio;
943
+ img.dataset.naturalWidth = naturalWidth;
944
+ img.dataset.naturalHeight = naturalHeight;
945
+ img.dataset.renderedWidth = renderedWidth;
946
+ img.dataset.renderedHeight = renderedHeight;
947
+
948
+ // Enable zoom if significantly scaled down
949
+ if (isScaledDown && maxZoomRatio > 1.1) {
950
+ img.classList.remove('no-zoom');
951
+ } else {
952
+ img.classList.add('no-zoom');
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Update counter display
958
+ */
959
+ updateCounter() {
960
+ if (this.options.showCounter && this.counter) {
961
+ this.counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
962
+ }
963
+ }
964
+
965
+ /**
966
+ * Update caption display
967
+ */
968
+ updateCaption() {
969
+ if (!this.caption || !this.items[this.currentIndex]) return;
970
+
971
+ const item = this.items[this.currentIndex];
972
+ const caption = item.dataset.caption || item.title || '';
973
+
974
+ if (caption) {
975
+ this.caption.textContent = caption;
976
+ this.caption.style.transform = 'translateX(-50%) translateY(0)';
977
+ this.caption.style.opacity = '1';
978
+ this.caption.style.pointerEvents = 'auto';
979
+ this.caption.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
980
+ } else {
981
+ this.caption.style.transform = 'translateX(-50%) translateY(100%)';
982
+ this.caption.style.opacity = '0';
983
+ this.caption.style.pointerEvents = 'none';
984
+ this.caption.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Update thumbnails display
990
+ */
991
+ updateThumbnails() {
992
+ if (!this.options.showThumbnails || !this.thumbs) return;
993
+
994
+ if (this.items.length === 1) {
995
+ this.caption.classList.add('single-item-thumbnail');
996
+ this.thumbs.style.display = 'none';
997
+ return;
998
+ }
999
+
1000
+ this.caption.classList.remove('single-item-thumbnail');
1001
+
1002
+ this.thumbs.style.display = 'flex';
1003
+ this.thumbs.innerHTML = '';
1004
+
1005
+ this.items.forEach((item, index) => {
1006
+ const thumb = document.createElement('div');
1007
+ thumb.className = 'zoomora-thumb';
1008
+ if (index === this.currentIndex) thumb.classList.add('active');
1009
+
1010
+ const img = document.createElement('img');
1011
+ const thumbSrc = this.getThumbnailSource(item);
1012
+
1013
+ if (thumbSrc) {
1014
+ img.src = thumbSrc;
1015
+ img.alt = item.alt || item.title || '';
1016
+ img.onerror = () => {
1017
+ thumb.style.display = 'none';
1018
+ };
1019
+ }
1020
+
1021
+ thumb.appendChild(img);
1022
+
1023
+ thumb.addEventListener('click', () => {
1024
+ this.currentIndex = index;
1025
+ this.loadContent();
1026
+ this.updateCounter();
1027
+ this.updateThumbnails();
1028
+ this.updateNavigation();
1029
+ this.updateCaption();
1030
+ });
1031
+
1032
+ this.thumbs.appendChild(thumb);
1033
+ });
1034
+ }
1035
+
1036
+ /**
1037
+ * Update navigation buttons
1038
+ */
1039
+ updateNavigation() {
1040
+ const prevBtn = document.getElementById('zoomoraPrevBtn');
1041
+ const nextBtn = document.getElementById('zoomoraNextBtn');
1042
+
1043
+ if (prevBtn) {
1044
+ prevBtn.style.display = this.currentIndex > 0 ? 'flex' : 'none';
1045
+ }
1046
+ if (nextBtn) {
1047
+ nextBtn.style.display = this.currentIndex < this.items.length - 1 ? 'flex' : 'none';
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Update control buttons
1053
+ */
1054
+ updateButtons() {
1055
+ const fullscreenBtn = document.getElementById('zoomoraFullscreenBtn');
1056
+ const collapseBtn = document.getElementById('zoomoraCollapseBtn');
1057
+
1058
+ if (fullscreenBtn && collapseBtn) {
1059
+ if (this.isFullscreen) {
1060
+ fullscreenBtn.style.display = 'none';
1061
+ collapseBtn.style.display = 'flex';
1062
+ } else {
1063
+ fullscreenBtn.style.display = 'flex';
1064
+ collapseBtn.style.display = 'none';
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ /**
1070
+ * Toggle zoom state
1071
+ */
1072
+ toggleZoom() {
1073
+ const media = this.content.querySelector('.zoomora-media');
1074
+ if (!media || media.tagName !== 'IMG' || media.classList.contains('no-zoom')) return;
1075
+
1076
+ this.isZoomed = !this.isZoomed;
1077
+ media.classList.toggle('zoomed', this.isZoomed);
1078
+
1079
+ if (this.isZoomed) {
1080
+ const zoomRatio = parseFloat(media.dataset.zoomRatio) || 2;
1081
+ let zoomScale = Math.min(zoomRatio, this.options.maxZoomScale);
1082
+
1083
+ if (zoomRatio > 2.5) {
1084
+ zoomScale = Math.min(zoomRatio * 0.7, 2.5);
1085
+ }
1086
+
1087
+ media.style.transform = `scale(${zoomScale})`;
1088
+ media.style.cursor = 'grab';
1089
+ media.dataset.currentZoomScale = zoomScale;
1090
+ this.currentScale = zoomScale;
1091
+ } else {
1092
+ media.style.transform = '';
1093
+ media.style.cursor = '';
1094
+ this.currentX = 0;
1095
+ this.currentY = 0;
1096
+ this.currentScale = 1;
1097
+ }
1098
+ }
1099
+
1100
+ /**
1101
+ * Toggle fullscreen mode
1102
+ */
1103
+ toggleFullscreen() {
1104
+ if (!this.isFullscreen) {
1105
+ this.enterFullscreen();
1106
+ } else {
1107
+ this.exitFullscreen();
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Enter fullscreen mode
1113
+ */
1114
+ enterFullscreen() {
1115
+ const element = this.zoomora;
1116
+ const requestFullscreen = element.requestFullscreen ||
1117
+ element.webkitRequestFullscreen ||
1118
+ element.mozRequestFullScreen ||
1119
+ element.msRequestFullscreen;
1120
+
1121
+ if (requestFullscreen) {
1122
+ requestFullscreen.call(element).then(() => {
1123
+ this.isFullscreen = true;
1124
+ this.zoomora.classList.add('fullscreen');
1125
+ this.updateButtons();
1126
+ }).catch(err => {
1127
+ console.warn('Zoomora: Could not enter fullscreen:', err);
1128
+ this.fallbackFullscreen();
1129
+ });
1130
+ } else {
1131
+ this.fallbackFullscreen();
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Exit fullscreen mode
1137
+ */
1138
+ exitFullscreen() {
1139
+ const isInFullscreen = document.fullscreenElement ||
1140
+ document.webkitFullscreenElement ||
1141
+ document.mozFullScreenElement ||
1142
+ document.msFullscreenElement;
1143
+
1144
+ if (isInFullscreen) {
1145
+ const exitFullscreen = document.exitFullscreen ||
1146
+ document.webkitExitFullscreen ||
1147
+ document.mozCancelFullScreen ||
1148
+ document.msExitFullscreen;
1149
+
1150
+ if (exitFullscreen) {
1151
+ exitFullscreen.call(document).then(() => {
1152
+ this.isFullscreen = false;
1153
+ this.zoomora.classList.remove('fullscreen');
1154
+ this.updateButtons();
1155
+ }).catch(err => {
1156
+ console.warn('Zoomora: Could not exit fullscreen:', err);
1157
+ this.fallbackExitFullscreen();
1158
+ });
1159
+ } else {
1160
+ this.fallbackExitFullscreen();
1161
+ }
1162
+ } else {
1163
+ this.fallbackExitFullscreen();
1164
+ }
1165
+ }
1166
+
1167
+ /**
1168
+ * Fallback fullscreen using CSS
1169
+ */
1170
+ fallbackFullscreen() {
1171
+ this.isFullscreen = true;
1172
+ this.zoomora.classList.add('fullscreen');
1173
+ this.updateButtons();
1174
+ }
1175
+
1176
+ /**
1177
+ * Exit CSS fullscreen
1178
+ */
1179
+ fallbackExitFullscreen() {
1180
+ this.isFullscreen = false;
1181
+ this.zoomora.classList.remove('fullscreen');
1182
+ this.updateButtons();
1183
+ }
1184
+
1185
+ /**
1186
+ * Toggle thumbnails visibility
1187
+ */
1188
+ toggleThumbnails() {
1189
+ if (this.items.length === 1) return;
1190
+
1191
+ this.thumbnailsVisible = !this.thumbnailsVisible;
1192
+ if (this.thumbs) {
1193
+ this.thumbs.classList.toggle('thumbs-hidden', !this.thumbnailsVisible);
1194
+
1195
+ if (!this.caption || !this.items[this.currentIndex]) return;
1196
+
1197
+ const item = this.items[this.currentIndex];
1198
+ const caption = item.dataset.caption || item.title || '';
1199
+
1200
+ if (caption && this.thumbs.classList.contains('thumbs-hidden')) {
1201
+ this.caption.style.bottom = '20px';
1202
+ this.caption.style.transition = 'bottom 0.3s ease, transform 0.3s ease';
1203
+ } else {
1204
+ this.caption.style.bottom = '120px';
1205
+ }
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Start drag operation
1211
+ * @param {Event} e - Mouse or touch event
1212
+ */
1213
+ startDrag(e) {
1214
+ const media = this.content.querySelector('.zoomora-media');
1215
+ if (!media || !this.isZoomed) return;
1216
+
1217
+ this.isDragging = true;
1218
+ media.style.cursor = 'grabbing';
1219
+
1220
+ this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1221
+ this.startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1222
+
1223
+ // Parse current transform
1224
+ const transform = media.style.transform;
1225
+ const scaleMatch = transform.match(/scale\(([^)]+)\)/);
1226
+ const translateMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
1227
+
1228
+ this.currentScale = scaleMatch ? parseFloat(scaleMatch[1]) : 1;
1229
+ this.currentX = translateMatch ? parseFloat(translateMatch[1]) : 0;
1230
+ this.currentY = translateMatch ? parseFloat(translateMatch[2]) : 0;
1231
+
1232
+ e.preventDefault();
1233
+ }
1234
+
1235
+ /**
1236
+ * Handle drag movement
1237
+ * @param {Event} e - Mouse or touch event
1238
+ */
1239
+ drag(e) {
1240
+ if (!this.isDragging) return;
1241
+
1242
+ e.preventDefault();
1243
+ const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1244
+ const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1245
+
1246
+ const deltaX = clientX - this.startX;
1247
+ const deltaY = clientY - this.startY;
1248
+
1249
+ const newX = this.currentX + deltaX;
1250
+ const newY = this.currentY + deltaY;
1251
+
1252
+ const media = this.content.querySelector('.zoomora-media');
1253
+ if (!media) return;
1254
+
1255
+ const mediaRect = media.getBoundingClientRect();
1256
+ const scale = this.currentScale;
1257
+ const maxX = (mediaRect.width * (scale - 1)) / 2;
1258
+ const maxY = (mediaRect.height * (scale - 1)) / 2;
1259
+
1260
+ const constrainedX = Math.max(-maxX, Math.min(maxX, newX));
1261
+ const constrainedY = Math.max(-maxY, Math.min(maxY, newY));
1262
+
1263
+ media.style.transform = `scale(${scale}) translate(${constrainedX}px, ${constrainedY}px)`;
1264
+ }
1265
+
1266
+ /**
1267
+ * End drag operation
1268
+ */
1269
+ endDrag() {
1270
+ if (!this.isDragging) return;
1271
+
1272
+ this.isDragging = false;
1273
+ const media = this.content.querySelector('.zoomora-media');
1274
+ if (media && this.isZoomed) {
1275
+ media.style.cursor = 'grab';
1276
+ }
1277
+ }
1278
+
1279
+ /**
1280
+ * Go to specific item by index
1281
+ * @param {number} index - Item index
1282
+ */
1283
+ goTo(index) {
1284
+ if (index < 0 || index >= this.items.length) return;
1285
+
1286
+ this.currentIndex = index;
1287
+ this.loadContent();
1288
+ this.updateCounter();
1289
+ this.updateThumbnails();
1290
+ this.updateNavigation();
1291
+ this.updateCaption();
1292
+ }
1293
+
1294
+ /**
1295
+ * Get current item index
1296
+ * @returns {number} - Current index
1297
+ */
1298
+ getCurrentIndex() {
1299
+ return this.currentIndex;
1300
+ }
1301
+
1302
+ /**
1303
+ * Get total number of items
1304
+ * @returns {number} - Total items
1305
+ */
1306
+ getTotalItems() {
1307
+ return this.items.length;
1308
+ }
1309
+
1310
+ /**
1311
+ * Check if lightbox is open
1312
+ * @returns {boolean} - True if open
1313
+ */
1314
+ isOpen() {
1315
+ return this.zoomora && this.zoomora.classList.contains('active');
1316
+ }
1317
+
1318
+ /**
1319
+ * Update options
1320
+ * @param {Object} newOptions - New options to merge
1321
+ */
1322
+ updateOptions(newOptions) {
1323
+ this.options = {...this.options, ...newOptions};
1324
+ }
1325
+
1326
+ /**
1327
+ * Refresh the lightbox (re-scan for items)
1328
+ */
1329
+ refresh() {
1330
+ if (this.isOpen()) {
1331
+ const currentItem = this.items[this.currentIndex];
1332
+ if (currentItem) {
1333
+ this.open(currentItem);
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ /**
1339
+ * Destroy the lightbox instance
1340
+ */
1341
+ destroy() {
1342
+ if (!this.isInitialized) return;
1343
+
1344
+ // Close if open
1345
+ if (this.isOpen()) {
1346
+ this.close();
1347
+ }
1348
+
1349
+ // Clean up auto-hide
1350
+ this.stopAutoHideTimer();
1351
+
1352
+ // Remove all event listeners
1353
+ this.eventListeners.forEach((listeners, key) => {
1354
+ listeners.forEach(({element, event, handler, options}) => {
1355
+ if (element && element.removeEventListener) {
1356
+ element.removeEventListener(event, handler, options);
1357
+ }
1358
+ });
1359
+ });
1360
+ this.eventListeners.clear();
1361
+
1362
+ // Remove DOM elements
1363
+ if (this.zoomora && this.zoomora.parentNode) {
1364
+ this.zoomora.parentNode.removeChild(this.zoomora);
1365
+ }
1366
+
1367
+ // Reset properties
1368
+ this.isInitialized = false;
1369
+ this.items = [];
1370
+ this.currentIndex = 0;
1371
+ this.zoomora = null;
1372
+ this.content = null;
1373
+ this.counter = null;
1374
+ this.thumbs = null;
1375
+ this.loading = null;
1376
+ this.caption = null;
1377
+ }
1378
+ }
1379
+
1380
+ // Export the class
1381
+ return Zoomora;
1382
+ }));