zoomora 1.3.0 → 1.4.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/README.md +122 -73
- package/dist/zoomora.css +2 -2
- package/dist/zoomora.es.js +1210 -1221
- package/dist/zoomora.es.js.map +1 -1
- package/dist/zoomora.es.min.js +508 -519
- package/dist/zoomora.umd.js +1200 -1215
- package/dist/zoomora.umd.js.map +1 -1
- package/dist/zoomora.umd.min.js +507 -522
- package/package.json +2 -1
- package/src/styles/zoomora.scss +23 -4
- package/src/zoomora.js +1463 -1434
package/src/zoomora.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Zoomora Lightbox Plugin v1.
|
|
2
|
+
* Zoomora Lightbox Plugin v1.4.0
|
|
3
3
|
* A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2026 Faruk Ahmed (FrontTheme)
|
|
@@ -9,153 +9,138 @@
|
|
|
9
9
|
// Import styles
|
|
10
10
|
import './styles/zoomora.scss';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Zoomora Lightbox Plugin
|
|
14
|
+
* @class
|
|
15
|
+
*/
|
|
16
|
+
class Zoomora {
|
|
17
|
+
/**
|
|
18
|
+
* Default configuration options
|
|
19
|
+
* @type {Object}
|
|
20
|
+
*/
|
|
21
|
+
static get defaults() {
|
|
22
|
+
return {
|
|
23
|
+
selector: '[data-zoomora]',
|
|
24
|
+
showCounter: true,
|
|
25
|
+
showThumbnails: true,
|
|
26
|
+
showFullscreen: true,
|
|
27
|
+
showZoom: true,
|
|
28
|
+
transition: 'fade', // 'fade' or 'slide'
|
|
29
|
+
useHref: false, // Use href instead of data-src
|
|
30
|
+
maxZoomScale: 3, // Maximum zoom scale
|
|
31
|
+
zoomStep: 0.1, // Zoom step for scroll wheel
|
|
32
|
+
animationDuration: 300, // Animation duration in ms
|
|
33
|
+
closeOnBackgroundClick: true, // Close lightbox when clicking on background
|
|
34
|
+
autoplayVideos: false, // Start YouTube and local videos when their slide opens
|
|
35
|
+
onOpen: null, // Callback when lightbox opens
|
|
36
|
+
onClose: null, // Callback when lightbox closes
|
|
37
|
+
onNext: null, // Callback when navigating to next item
|
|
38
|
+
onPrev: null, // Callback when navigating to previous item
|
|
14
39
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
define(factory);
|
|
21
|
-
} else {
|
|
22
|
-
// Browser globals
|
|
23
|
-
global.Zoomora = factory();
|
|
40
|
+
// Auto-hide controls
|
|
41
|
+
showAutoHideToggle: true, // Show auto-hide toggle button
|
|
42
|
+
autoHideDelay: 3000, // Delay in ms before hiding controls
|
|
43
|
+
autoHideEnabled: false, // Default state of auto-hide
|
|
44
|
+
};
|
|
24
45
|
}
|
|
25
|
-
}(typeof window !== 'undefined' ? window : this, function () {
|
|
26
|
-
'use strict';
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
48
|
+
* Constructor
|
|
49
|
+
* @param {Object} options - Configuration options
|
|
31
50
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.isDragging = false;
|
|
73
|
-
this.thumbnailsVisible = true;
|
|
74
|
-
this.startX = 0;
|
|
75
|
-
this.startY = 0;
|
|
76
|
-
this.currentX = 0;
|
|
77
|
-
this.currentY = 0;
|
|
78
|
-
this.currentScale = 1;
|
|
79
|
-
this.isInitialized = false;
|
|
80
|
-
this.eventListeners = new Map();
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.options = {...Zoomora.defaults, ...options};
|
|
53
|
+
this.currentIndex = 0;
|
|
54
|
+
this.items = [];
|
|
55
|
+
this.isZoomed = false;
|
|
56
|
+
this.isFullscreen = false;
|
|
57
|
+
this.isDragging = false;
|
|
58
|
+
this.thumbnailsVisible = true;
|
|
59
|
+
this.startX = 0;
|
|
60
|
+
this.startY = 0;
|
|
61
|
+
this.currentX = 0;
|
|
62
|
+
this.currentY = 0;
|
|
63
|
+
this.currentScale = 1;
|
|
64
|
+
this.isInitialized = false;
|
|
65
|
+
this.eventListeners = new Map();
|
|
66
|
+
|
|
67
|
+
// Auto-hide controls
|
|
68
|
+
this.autoHideEnabled = false;
|
|
69
|
+
this.autoHideTimeout = null;
|
|
70
|
+
this.autoHideDelay = 3000;
|
|
71
|
+
this.isControlsVisible = true;
|
|
72
|
+
|
|
73
|
+
this.lastDragTime = 0;
|
|
74
|
+
this.boundMethods = {
|
|
75
|
+
handleDocumentClick: this.handleDocumentClick.bind(this),
|
|
76
|
+
handleKeydown: this.handleKeydown.bind(this),
|
|
77
|
+
handleFullscreenChange: this.handleFullscreenChange.bind(this),
|
|
78
|
+
handleZoomoraClick: this.handleZoomoraClick.bind(this),
|
|
79
|
+
handleContentClick: this.handleContentClick.bind(this),
|
|
80
|
+
handleMouseDown: this.handleMouseDown.bind(this),
|
|
81
|
+
handleMouseMove: this.handleMouseMove.bind(this),
|
|
82
|
+
handleMouseUp: this.handleMouseUp.bind(this),
|
|
83
|
+
handleTouchStart: this.handleTouchStart.bind(this),
|
|
84
|
+
handleTouchMove: this.handleTouchMove.bind(this),
|
|
85
|
+
handleTouchEnd: this.handleTouchEnd.bind(this),
|
|
86
|
+
handleWheel: this.handleWheel.bind(this),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.init();
|
|
90
|
+
}
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
handleWheel: this.handleWheel.bind(this),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
this.init();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Initialize the plugin
|
|
109
|
-
*/
|
|
110
|
-
init() {
|
|
111
|
-
if (this.isInitialized) {
|
|
112
|
-
console.warn('Zoomora: Plugin already initialized');
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
92
|
+
/**
|
|
93
|
+
* Initialize the plugin
|
|
94
|
+
*/
|
|
95
|
+
init() {
|
|
96
|
+
if (this.isInitialized) {
|
|
97
|
+
console.warn('Zoomora: Plugin already initialized');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
try {
|
|
102
|
+
this.createLightbox();
|
|
103
|
+
this.bindEvents();
|
|
119
104
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
105
|
+
this.isInitialized = true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Zoomora: Initialization failed', error);
|
|
124
108
|
}
|
|
109
|
+
}
|
|
125
110
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Static method to bind to anchor tags with href
|
|
113
|
+
* @param {string} selector - CSS selector
|
|
114
|
+
* @param {Object} options - Configuration options
|
|
115
|
+
* @returns {Zoomora} - New Zoomora instance
|
|
116
|
+
*/
|
|
117
|
+
static bind(selector, options = {}) {
|
|
118
|
+
const enhancedOptions = {
|
|
119
|
+
...options,
|
|
120
|
+
selector: selector,
|
|
121
|
+
useHref: true
|
|
122
|
+
};
|
|
123
|
+
return new Zoomora(enhancedOptions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create lightbox HTML structure
|
|
128
|
+
*/
|
|
129
|
+
createLightbox() {
|
|
130
|
+
// Check if lightbox already exists
|
|
131
|
+
if (document.getElementById('zoomoraLightbox')) {
|
|
132
|
+
console.warn('Zoomora: Lightbox already exists in DOM');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
|
|
136
|
+
// Build auto-hide button conditionally
|
|
137
|
+
const autoHideButtonHTML = this.options.showAutoHideToggle ? `
|
|
153
138
|
<button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
|
|
154
139
|
<i class="zoomorai-controls-off"></i>
|
|
155
140
|
</button>
|
|
156
141
|
` : '';
|
|
157
142
|
|
|
158
|
-
|
|
143
|
+
const zoomoraHTML = `
|
|
159
144
|
<div class="zoomora" id="zoomoraLightbox">
|
|
160
145
|
<div class="zoomora-container">
|
|
161
146
|
<div class="zoomora-header" id="zoomoraHeader">
|
|
@@ -204,1560 +189,1604 @@ import './styles/zoomora.scss';
|
|
|
204
189
|
</div>
|
|
205
190
|
`;
|
|
206
191
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
192
|
+
document.body.insertAdjacentHTML('beforeend', zoomoraHTML);
|
|
193
|
+
|
|
194
|
+
// Cache DOM elements
|
|
195
|
+
this.zoomora = document.getElementById('zoomoraLightbox');
|
|
196
|
+
this.header = document.getElementById('zoomoraHeader');
|
|
197
|
+
this.content = document.getElementById('zoomoraContent');
|
|
198
|
+
this.slidesContainer = document.getElementById('zoomoraSlidesContainer');
|
|
199
|
+
this.counter = document.getElementById('zoomoraCounter');
|
|
200
|
+
this.thumbs = document.getElementById('zoomoraThumbs');
|
|
201
|
+
this.loading = document.getElementById('zoomoraLoading');
|
|
202
|
+
this.caption = document.getElementById('zoomoraCaption');
|
|
203
|
+
this.prevBtn = document.getElementById('zoomoraPrevBtn');
|
|
204
|
+
this.nextBtn = document.getElementById('zoomoraNextBtn');
|
|
205
|
+
|
|
206
|
+
// Verify all elements were created
|
|
207
|
+
if (!this.zoomora || !this.content || !this.counter || !this.thumbs || !this.loading || !this.caption) {
|
|
208
|
+
throw new Error('Failed to create required DOM elements');
|
|
225
209
|
}
|
|
210
|
+
}
|
|
226
211
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Bind event listeners
|
|
214
|
+
*/
|
|
215
|
+
bindEvents() {
|
|
216
|
+
// Document click handler for opening lightbox
|
|
217
|
+
this.addEventListener(document, 'click', this.boundMethods.handleDocumentClick);
|
|
218
|
+
|
|
219
|
+
// Control button handlers
|
|
220
|
+
this.addEventListener(document.getElementById('zoomoraCloseBtn'), 'click', () => this.close());
|
|
221
|
+
this.addEventListener(document.getElementById('zoomoraPrevBtn'), 'click', () => this.prev());
|
|
222
|
+
this.addEventListener(document.getElementById('zoomoraNextBtn'), 'click', () => this.next());
|
|
223
|
+
if (this.options.showZoom) {
|
|
224
|
+
this.addEventListener(document.getElementById('zoomoraZoomBtn'), 'click', () => this.toggleZoom());
|
|
225
|
+
}
|
|
226
|
+
if (this.options.showFullscreen) {
|
|
227
|
+
this.addEventListener(document.getElementById('zoomoraFullscreenBtn'), 'click', () => this.toggleFullscreen());
|
|
228
|
+
this.addEventListener(document.getElementById('zoomoraCollapseBtn'), 'click', () => this.exitFullscreen());
|
|
229
|
+
}
|
|
230
|
+
if (this.options.showThumbnails) {
|
|
231
|
+
this.addEventListener(document.getElementById('zoomoraThumbnailBtn'), 'click', () => this.toggleThumbnails());
|
|
232
|
+
}
|
|
248
233
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
234
|
+
// Fullscreen change events
|
|
235
|
+
this.addEventListener(document, 'fullscreenchange', this.boundMethods.handleFullscreenChange);
|
|
236
|
+
this.addEventListener(document, 'webkitfullscreenchange', this.boundMethods.handleFullscreenChange);
|
|
237
|
+
this.addEventListener(document, 'mozfullscreenchange', this.boundMethods.handleFullscreenChange);
|
|
238
|
+
this.addEventListener(document, 'MSFullscreenChange', this.boundMethods.handleFullscreenChange);
|
|
254
239
|
|
|
255
|
-
|
|
256
|
-
|
|
240
|
+
// Keyboard navigation
|
|
241
|
+
this.addEventListener(document, 'keydown', this.boundMethods.handleKeydown);
|
|
257
242
|
|
|
258
|
-
|
|
259
|
-
|
|
243
|
+
// Lightbox click to close
|
|
244
|
+
this.addEventListener(this.zoomora, 'click', this.boundMethods.handleZoomoraClick);
|
|
260
245
|
|
|
261
|
-
|
|
262
|
-
|
|
246
|
+
// Content click for zoom toggle and background click
|
|
247
|
+
this.addEventListener(this.content, 'click', this.boundMethods.handleContentClick);
|
|
263
248
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
249
|
+
// Mouse drag events
|
|
250
|
+
this.addEventListener(this.content, 'mousedown', this.boundMethods.handleMouseDown);
|
|
251
|
+
this.addEventListener(this.content, 'mousemove', this.boundMethods.handleMouseMove);
|
|
252
|
+
this.addEventListener(this.content, 'mouseup', this.boundMethods.handleMouseUp);
|
|
253
|
+
this.addEventListener(this.content, 'mouseleave', this.boundMethods.handleMouseUp);
|
|
269
254
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
255
|
+
// Touch events
|
|
256
|
+
this.addEventListener(this.content, 'touchstart', this.boundMethods.handleTouchStart, {passive: false});
|
|
257
|
+
this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
|
|
258
|
+
this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
|
|
274
259
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
260
|
+
// Scroll-wheel zoom
|
|
261
|
+
if (this.options.showZoom) {
|
|
262
|
+
this.addEventListener(this.content, 'wheel', this.boundMethods.handleWheel, {passive: false});
|
|
263
|
+
}
|
|
279
264
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
265
|
+
// Add auto-hide toggle button handler (only if enabled)
|
|
266
|
+
if (this.options.showAutoHideToggle) {
|
|
267
|
+
this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
|
|
283
268
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
269
|
+
// Add mouse/touch activity listeners for auto-hide
|
|
270
|
+
this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
|
|
271
|
+
this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
|
|
272
|
+
this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
|
|
289
273
|
}
|
|
274
|
+
}
|
|
290
275
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Add event listener with cleanup tracking
|
|
278
|
+
* @param {EventTarget} element - DOM element or document
|
|
279
|
+
* @param {string} event - Event type
|
|
280
|
+
* @param {Function} handler - Event handler
|
|
281
|
+
* @param {Object} options - Event options
|
|
282
|
+
*/
|
|
283
|
+
addEventListener(element, event, handler, options = {}) {
|
|
284
|
+
if (!element) return;
|
|
300
285
|
|
|
301
|
-
|
|
286
|
+
element.addEventListener(event, handler, options);
|
|
302
287
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
this.eventListeners.get(key).push({element, event, handler, options});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Handle document click events
|
|
313
|
-
* @param {Event} e - Click event
|
|
314
|
-
*/
|
|
315
|
-
handleDocumentClick(e) {
|
|
316
|
-
const target = e.target.closest(this.options.selector);
|
|
317
|
-
if (target) {
|
|
318
|
-
e.preventDefault();
|
|
319
|
-
this.open(target);
|
|
320
|
-
}
|
|
288
|
+
// Track for cleanup
|
|
289
|
+
const key = `${element.constructor.name}_${event}`;
|
|
290
|
+
if (!this.eventListeners.has(key)) {
|
|
291
|
+
this.eventListeners.set(key, []);
|
|
321
292
|
}
|
|
293
|
+
this.eventListeners.get(key).push({element, event, handler, options});
|
|
294
|
+
}
|
|
322
295
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (this.isFullscreen) {
|
|
333
|
-
this.exitFullscreen();
|
|
334
|
-
} else {
|
|
335
|
-
this.close();
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
case 'ArrowLeft':
|
|
339
|
-
this.prev();
|
|
340
|
-
break;
|
|
341
|
-
case 'ArrowRight':
|
|
342
|
-
this.next();
|
|
343
|
-
break;
|
|
344
|
-
case 'z':
|
|
345
|
-
case 'Z':
|
|
346
|
-
this.toggleZoom();
|
|
347
|
-
break;
|
|
348
|
-
case 'f':
|
|
349
|
-
case 'F':
|
|
350
|
-
this.toggleFullscreen();
|
|
351
|
-
break;
|
|
352
|
-
case 't':
|
|
353
|
-
case 'T':
|
|
354
|
-
this.toggleThumbnails();
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
296
|
+
/**
|
|
297
|
+
* Handle document click events
|
|
298
|
+
* @param {Event} e - Click event
|
|
299
|
+
*/
|
|
300
|
+
handleDocumentClick(e) {
|
|
301
|
+
const target = e.target.closest(this.options.selector);
|
|
302
|
+
if (target) {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
this.open(target);
|
|
357
305
|
}
|
|
306
|
+
}
|
|
358
307
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
308
|
+
/**
|
|
309
|
+
* Handle keyboard events
|
|
310
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
311
|
+
*/
|
|
312
|
+
handleKeydown(e) {
|
|
313
|
+
if (!this.zoomora.classList.contains('active')) {
|
|
314
|
+
this.handleTriggerKeydown(e);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
369
317
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.isFullscreen
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
this.
|
|
380
|
-
|
|
381
|
-
|
|
318
|
+
switch (e.key) {
|
|
319
|
+
case 'Escape':
|
|
320
|
+
if (this.isFullscreen) {
|
|
321
|
+
this.exitFullscreen();
|
|
322
|
+
} else {
|
|
323
|
+
this.close();
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
case 'ArrowLeft':
|
|
327
|
+
this.prev();
|
|
328
|
+
break;
|
|
329
|
+
case 'ArrowRight':
|
|
330
|
+
this.next();
|
|
331
|
+
break;
|
|
332
|
+
case 'z':
|
|
333
|
+
case 'Z':
|
|
334
|
+
this.toggleZoom();
|
|
335
|
+
break;
|
|
336
|
+
case 'f':
|
|
337
|
+
case 'F':
|
|
338
|
+
this.toggleFullscreen();
|
|
339
|
+
break;
|
|
340
|
+
case 't':
|
|
341
|
+
case 'T':
|
|
342
|
+
this.toggleThumbnails();
|
|
343
|
+
break;
|
|
382
344
|
}
|
|
345
|
+
}
|
|
383
346
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Open the lightbox with Enter or Space on a focused trigger
|
|
349
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
350
|
+
*/
|
|
351
|
+
handleTriggerKeydown(e) {
|
|
352
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
353
|
+
|
|
354
|
+
const trigger = e.target.closest(this.options.selector);
|
|
355
|
+
if (!trigger) return;
|
|
356
|
+
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
this.open(trigger);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Handle fullscreen change events
|
|
363
|
+
*/
|
|
364
|
+
handleFullscreenChange() {
|
|
365
|
+
const isInFullscreen = !!(
|
|
366
|
+
document.fullscreenElement ||
|
|
367
|
+
document.webkitFullscreenElement ||
|
|
368
|
+
document.mozFullScreenElement ||
|
|
369
|
+
document.msFullscreenElement
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// Only update if state changed from fullscreen to not fullscreen
|
|
373
|
+
if (!isInFullscreen && this.isFullscreen) {
|
|
374
|
+
this.isFullscreen = false;
|
|
375
|
+
this.zoomora.classList.remove('fullscreen');
|
|
376
|
+
this.updateButtons();
|
|
377
|
+
}
|
|
378
|
+
// Update if entering fullscreen
|
|
379
|
+
else if (isInFullscreen && !this.isFullscreen) {
|
|
380
|
+
this.isFullscreen = true;
|
|
381
|
+
this.zoomora.classList.add('fullscreen');
|
|
382
|
+
this.updateButtons();
|
|
392
383
|
}
|
|
384
|
+
}
|
|
393
385
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Handle zoomora container clicks
|
|
388
|
+
* @param {Event} e - Click event
|
|
389
|
+
*/
|
|
390
|
+
handleZoomoraClick(e) {
|
|
391
|
+
if (e.target === this.zoomora && this.options.closeOnBackgroundClick) {
|
|
392
|
+
this.close();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
403
395
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Handle content clicks
|
|
398
|
+
* @param {Event} e - Click event
|
|
399
|
+
*/
|
|
400
|
+
handleContentClick(e) {
|
|
401
|
+
// Ignore click if we just finished dragging (within 200ms)
|
|
402
|
+
if (Date.now() - this.lastDragTime < 200) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
408
405
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
406
|
+
// Don't close on header clicks
|
|
407
|
+
if (this.header && this.header.contains(e.target)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
413
410
|
|
|
414
|
-
|
|
415
|
-
|
|
411
|
+
// Don't close on button clicks
|
|
412
|
+
if (e.target.closest('button')) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (mediaElement.classList.contains('zoomora-media') && !mediaElement.classList.contains('no-zoom')) {
|
|
420
|
-
this.toggleZoom();
|
|
421
|
-
}
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
416
|
+
// Check if click is directly on actual media element (img, video, iframe)
|
|
417
|
+
const mediaElement = e.target.closest('.zoomora-media, .zoomora-video, .zoomora-iframe');
|
|
424
418
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
if (e.target.closest('.zoomora-play-button')) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
// If clicking on background area of video container, close
|
|
433
|
-
if (this.options.closeOnBackgroundClick) {
|
|
434
|
-
this.close();
|
|
435
|
-
}
|
|
436
|
-
return;
|
|
419
|
+
if (mediaElement) {
|
|
420
|
+
// Only toggle zoom for images
|
|
421
|
+
if (mediaElement.classList.contains('zoomora-media') && !mediaElement.classList.contains('no-zoom')) {
|
|
422
|
+
this.toggleZoom();
|
|
437
423
|
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
438
426
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
clickedElement.classList.contains('zoomora-slide-container') ||
|
|
445
|
-
clickedElement.classList.contains('zoomora-slide') ||
|
|
446
|
-
clickedElement.classList.contains('zoomora-slides-container') ||
|
|
447
|
-
clickedElement.id === 'zoomoraContent' ||
|
|
448
|
-
clickedElement.id === 'zoomoraSlidesContainer';
|
|
449
|
-
|
|
450
|
-
if (isBackgroundElement && this.options.closeOnBackgroundClick) {
|
|
451
|
-
this.close();
|
|
427
|
+
// Check if clicking inside video container but not on video itself
|
|
428
|
+
const videoContainer = e.target.closest('.zoomora-video-container');
|
|
429
|
+
if (videoContainer) {
|
|
430
|
+
// If clicking on the play button, don't close
|
|
431
|
+
if (e.target.closest('.zoomora-play-button')) {
|
|
452
432
|
return;
|
|
453
433
|
}
|
|
454
|
-
|
|
455
|
-
// If we reach here and no media was clicked, close
|
|
434
|
+
// If clicking on background area of video container, close
|
|
456
435
|
if (this.options.closeOnBackgroundClick) {
|
|
457
436
|
this.close();
|
|
458
437
|
}
|
|
438
|
+
return;
|
|
459
439
|
}
|
|
460
440
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
441
|
+
// Check if clicking on background elements (including when zoomed)
|
|
442
|
+
// This checks the actual target element, not just closest()
|
|
443
|
+
const clickedElement = e.target;
|
|
444
|
+
const isBackgroundElement =
|
|
445
|
+
clickedElement.classList.contains('zoomora-content') ||
|
|
446
|
+
clickedElement.classList.contains('zoomora-slide-container') ||
|
|
447
|
+
clickedElement.classList.contains('zoomora-slide') ||
|
|
448
|
+
clickedElement.classList.contains('zoomora-slides-container') ||
|
|
449
|
+
clickedElement.id === 'zoomoraContent' ||
|
|
450
|
+
clickedElement.id === 'zoomoraSlidesContainer';
|
|
451
|
+
|
|
452
|
+
if (isBackgroundElement && this.options.closeOnBackgroundClick) {
|
|
453
|
+
this.close();
|
|
454
|
+
return;
|
|
467
455
|
}
|
|
468
456
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
*/
|
|
473
|
-
handleMouseMove(e) {
|
|
474
|
-
this.drag(e);
|
|
457
|
+
// If we reach here and no media was clicked, close
|
|
458
|
+
if (this.options.closeOnBackgroundClick) {
|
|
459
|
+
this.close();
|
|
475
460
|
}
|
|
461
|
+
}
|
|
476
462
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Handle mouse down events
|
|
465
|
+
* @param {MouseEvent} e - Mouse event
|
|
466
|
+
*/
|
|
467
|
+
handleMouseDown(e) {
|
|
468
|
+
this.startDrag(e);
|
|
469
|
+
}
|
|
483
470
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Handle mouse move events
|
|
473
|
+
* @param {MouseEvent} e - Mouse event
|
|
474
|
+
*/
|
|
475
|
+
handleMouseMove(e) {
|
|
476
|
+
this.drag(e);
|
|
477
|
+
}
|
|
491
478
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
479
|
+
/**
|
|
480
|
+
* Handle mouse up events
|
|
481
|
+
*/
|
|
482
|
+
handleMouseUp() {
|
|
483
|
+
this.endDrag();
|
|
484
|
+
}
|
|
499
485
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Handle touch start events
|
|
488
|
+
* @param {TouchEvent} e - Touch event
|
|
489
|
+
*/
|
|
490
|
+
handleTouchStart(e) {
|
|
491
|
+
this.startDrag(e);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Handle touch move events
|
|
496
|
+
* @param {TouchEvent} e - Touch event
|
|
497
|
+
*/
|
|
498
|
+
handleTouchMove(e) {
|
|
499
|
+
this.drag(e);
|
|
500
|
+
}
|
|
506
501
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (!media || media.tagName !== 'IMG') return;
|
|
502
|
+
/**
|
|
503
|
+
* Handle touch end events
|
|
504
|
+
*/
|
|
505
|
+
handleTouchEnd() {
|
|
506
|
+
this.endDrag();
|
|
507
|
+
}
|
|
514
508
|
|
|
515
|
-
|
|
516
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Handle scroll-wheel zoom
|
|
511
|
+
* @param {WheelEvent} e - Wheel event
|
|
512
|
+
*/
|
|
513
|
+
handleWheel(e) {
|
|
514
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
515
|
+
if (!media || media.tagName !== 'IMG') return;
|
|
517
516
|
|
|
518
|
-
|
|
517
|
+
const zoomConfig = this.calculateZoomLevels(media);
|
|
518
|
+
if (!zoomConfig.canZoom) return;
|
|
519
519
|
|
|
520
|
-
|
|
521
|
-
const step = this.options.zoomStep || 0.1;
|
|
522
|
-
const direction = e.deltaY < 0 ? 1 : -1; // scroll/pinch up = zoom in
|
|
520
|
+
e.preventDefault();
|
|
523
521
|
|
|
524
|
-
|
|
525
|
-
|
|
522
|
+
const maxScale = zoomConfig.levels[zoomConfig.levels.length - 1];
|
|
523
|
+
const step = this.options.zoomStep || 0.1;
|
|
524
|
+
const direction = e.deltaY < 0 ? 1 : -1; // scroll/pinch up = zoom in
|
|
526
525
|
|
|
527
|
-
|
|
526
|
+
let newScale = (this.currentScale || 1) + (direction * step * maxScale);
|
|
527
|
+
newScale = Math.max(1, Math.min(maxScale, newScale));
|
|
528
528
|
|
|
529
|
-
|
|
530
|
-
this.isZoomed = newScale > 1;
|
|
531
|
-
this.currentX = 0;
|
|
532
|
-
this.currentY = 0;
|
|
529
|
+
if (newScale === this.currentScale) return;
|
|
533
530
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
} else {
|
|
539
|
-
media.style.transform = '';
|
|
540
|
-
media.style.cursor = 'pointer';
|
|
541
|
-
media.classList.remove('zoomed');
|
|
542
|
-
}
|
|
531
|
+
this.currentScale = newScale;
|
|
532
|
+
this.isZoomed = newScale > 1;
|
|
533
|
+
this.currentX = 0;
|
|
534
|
+
this.currentY = 0;
|
|
543
535
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
media.dataset.zoomLevel = nearestIndex.toString();
|
|
536
|
+
if (this.isZoomed) {
|
|
537
|
+
media.style.transform = `scale(${newScale})`;
|
|
538
|
+
media.style.cursor = 'grab';
|
|
539
|
+
media.classList.add('zoomed');
|
|
540
|
+
} else {
|
|
541
|
+
media.style.transform = '';
|
|
542
|
+
media.style.cursor = 'pointer';
|
|
543
|
+
media.classList.remove('zoomed');
|
|
553
544
|
}
|
|
554
545
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const src = item.dataset.src || item.src || item.getAttribute('src');
|
|
562
|
-
const type = item.dataset.type || 'image';
|
|
563
|
-
|
|
564
|
-
// Handle YouTube URLs for video types
|
|
565
|
-
if (type === 'video' && src) {
|
|
566
|
-
return this.convertToEmbedUrl(src);
|
|
546
|
+
// Keep the click-to-cycle zoom index roughly in sync with the
|
|
547
|
+
// continuous wheel scale, so a click after scrolling cycles sensibly.
|
|
548
|
+
let nearestIndex = 0;
|
|
549
|
+
zoomConfig.levels.forEach((level, index) => {
|
|
550
|
+
if (Math.abs(level - newScale) < Math.abs(zoomConfig.levels[nearestIndex] - newScale)) {
|
|
551
|
+
nearestIndex = index;
|
|
567
552
|
}
|
|
553
|
+
});
|
|
554
|
+
media.dataset.zoomLevel = nearestIndex.toString();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get image source from element
|
|
559
|
+
* @param {Element} item - DOM element
|
|
560
|
+
* @returns {string} - Image source URL
|
|
561
|
+
*/
|
|
562
|
+
getImageSource(item) {
|
|
563
|
+
const src = item.dataset.src || item.getAttribute('href') || item.src || item.getAttribute('src');
|
|
564
|
+
const type = item.dataset.type || 'image';
|
|
568
565
|
|
|
569
|
-
|
|
566
|
+
// Handle YouTube URLs for video types
|
|
567
|
+
if (type === 'video' && src) {
|
|
568
|
+
return this.convertToEmbedUrl(src);
|
|
570
569
|
}
|
|
571
570
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* @param url
|
|
575
|
-
* @returns {*|string}
|
|
576
|
-
*/
|
|
577
|
-
convertToEmbedUrl(url) {
|
|
578
|
-
// If already an embed URL, return as is
|
|
579
|
-
if (url.includes('youtube.com/embed/')) {
|
|
580
|
-
return url;
|
|
581
|
-
}
|
|
571
|
+
return src;
|
|
572
|
+
}
|
|
582
573
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Convert YouTube URLs to embed format
|
|
576
|
+
* @param url
|
|
577
|
+
* @returns {*|string}
|
|
578
|
+
*/
|
|
579
|
+
convertToEmbedUrl(url) {
|
|
580
|
+
// If already an embed URL, return as is
|
|
581
|
+
if (url.includes('youtube.com/embed/')) {
|
|
582
|
+
return url;
|
|
583
|
+
}
|
|
586
584
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
585
|
+
// Handle different YouTube URL formats
|
|
586
|
+
let videoId = null;
|
|
587
|
+
let startTime = null;
|
|
588
|
+
|
|
589
|
+
// Extract video ID and start time from different YouTube URL formats
|
|
590
|
+
if (url.includes('youtube.com/watch')) {
|
|
591
|
+
const urlParams = new URLSearchParams(url.split('?')[1]);
|
|
592
|
+
videoId = urlParams.get('v');
|
|
593
|
+
const t = urlParams.get('t');
|
|
594
|
+
if (t) {
|
|
595
|
+
// Convert time format (e.g., "111s" to seconds)
|
|
596
|
+
startTime = t.replace('s', '');
|
|
597
|
+
}
|
|
598
|
+
} else if (url.includes('youtu.be/')) {
|
|
599
|
+
const parts = url.split('/');
|
|
600
|
+
const videoAndParams = parts[parts.length - 1];
|
|
601
|
+
if (videoAndParams.includes('?')) {
|
|
602
|
+
videoId = videoAndParams.split('?')[0];
|
|
603
|
+
const urlParams = new URLSearchParams(videoAndParams.split('?')[1]);
|
|
591
604
|
const t = urlParams.get('t');
|
|
592
|
-
if (t)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
} else if (url.includes('youtu.be/')) {
|
|
597
|
-
const parts = url.split('/');
|
|
598
|
-
const videoAndParams = parts[parts.length - 1];
|
|
599
|
-
if (videoAndParams.includes('?')) {
|
|
600
|
-
videoId = videoAndParams.split('?')[0];
|
|
601
|
-
const urlParams = new URLSearchParams(videoAndParams.split('?')[1]);
|
|
602
|
-
const t = urlParams.get('t');
|
|
603
|
-
if (t) startTime = t.replace('s', '');
|
|
604
|
-
} else {
|
|
605
|
-
videoId = videoAndParams;
|
|
606
|
-
}
|
|
605
|
+
if (t) startTime = t.replace('s', '');
|
|
606
|
+
} else {
|
|
607
|
+
videoId = videoAndParams;
|
|
607
608
|
}
|
|
609
|
+
}
|
|
608
610
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (startTime) {
|
|
614
|
-
params.append('start', startTime);
|
|
615
|
-
}
|
|
611
|
+
if (videoId) {
|
|
612
|
+
let embedUrl = `https://www.youtube.com/embed/${videoId}`;
|
|
613
|
+
const params = new URLSearchParams();
|
|
616
614
|
|
|
617
|
-
|
|
618
|
-
params.append('
|
|
619
|
-
|
|
615
|
+
if (startTime) {
|
|
616
|
+
params.append('start', startTime);
|
|
617
|
+
}
|
|
620
618
|
|
|
621
|
-
|
|
622
|
-
|
|
619
|
+
// Add other useful parameters
|
|
620
|
+
params.append('rel', '0'); // Don't show related videos
|
|
621
|
+
params.append('modestbranding', '1'); // Modest branding
|
|
623
622
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
623
|
+
if (this.options.autoplayVideos) {
|
|
624
|
+
params.append('autoplay', '1');
|
|
625
|
+
}
|
|
627
626
|
|
|
628
|
-
|
|
627
|
+
if (params.toString()) {
|
|
628
|
+
embedUrl += '?' + params.toString();
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
|
|
632
|
-
return url;
|
|
631
|
+
return embedUrl;
|
|
633
632
|
}
|
|
634
633
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
* @returns {string} - Thumbnail source URL
|
|
639
|
-
*/
|
|
640
|
-
getThumbnailSource(item) {
|
|
641
|
-
const type = item.dataset.type || 'image';
|
|
634
|
+
// If not a YouTube URL or couldn't parse, return original
|
|
635
|
+
return url;
|
|
636
|
+
}
|
|
642
637
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
638
|
+
/**
|
|
639
|
+
* Get thumbnail source from element
|
|
640
|
+
* Order: data-thumbnail, the trigger's own image or the image inside it, then data-src or href.
|
|
641
|
+
* @param {Element|Object} item - DOM element or virtual item
|
|
642
|
+
* @returns {string} - Thumbnail source URL
|
|
643
|
+
*/
|
|
644
|
+
getThumbnailSource(item) {
|
|
645
|
+
if (item.dataset.thumbnail) {
|
|
646
|
+
return item.dataset.thumbnail;
|
|
647
|
+
}
|
|
647
648
|
|
|
648
|
-
|
|
649
|
-
|
|
649
|
+
const innerImage = item.querySelector?.('img');
|
|
650
|
+
const imageSrc = item.getAttribute('src') || innerImage?.getAttribute('src');
|
|
651
|
+
const type = item.dataset.type || 'image';
|
|
652
|
+
|
|
653
|
+
if (type === 'video') {
|
|
654
|
+
return imageSrc;
|
|
650
655
|
}
|
|
651
656
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
657
|
+
return imageSrc || item.dataset.src || item.getAttribute('href');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Get group identifier from element
|
|
662
|
+
* @param {Element} element - DOM element
|
|
663
|
+
* @returns {string} - Group identifier
|
|
664
|
+
*/
|
|
665
|
+
getGroup(element) {
|
|
666
|
+
if (this.options.useHref) {
|
|
667
|
+
return element.dataset.lightbox || element.dataset.gallery || 'default';
|
|
662
668
|
}
|
|
669
|
+
return element.dataset.zoomora || 'default';
|
|
670
|
+
}
|
|
663
671
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Open lightbox with specified element
|
|
674
|
+
* @param {Element} element - DOM element to open
|
|
675
|
+
*/
|
|
676
|
+
open(element) {
|
|
677
|
+
if (!element) return;
|
|
670
678
|
|
|
671
|
-
|
|
672
|
-
|
|
679
|
+
const group = this.getGroup(element);
|
|
680
|
+
let groupSelector;
|
|
673
681
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
} else {
|
|
680
|
-
groupSelector = this.options.selector;
|
|
681
|
-
}
|
|
682
|
+
if (this.options.useHref) {
|
|
683
|
+
if (element.dataset.lightbox) {
|
|
684
|
+
groupSelector = `[data-lightbox="${group}"]`;
|
|
685
|
+
} else if (element.dataset.gallery) {
|
|
686
|
+
groupSelector = `[data-gallery="${group}"]`;
|
|
682
687
|
} else {
|
|
683
|
-
groupSelector =
|
|
688
|
+
groupSelector = this.options.selector;
|
|
684
689
|
}
|
|
690
|
+
} else {
|
|
691
|
+
groupSelector = `[data-zoomora="${group}"]`;
|
|
692
|
+
}
|
|
685
693
|
|
|
686
|
-
|
|
687
|
-
|
|
694
|
+
this.items = Array.from(document.querySelectorAll(groupSelector));
|
|
695
|
+
this.currentIndex = Math.max(0, this.items.indexOf(element));
|
|
688
696
|
|
|
689
|
-
|
|
697
|
+
this._activateLightbox();
|
|
690
698
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
699
|
+
// Trigger callback
|
|
700
|
+
if (typeof this.options.onOpen === 'function') {
|
|
701
|
+
this.options.onOpen(element, this.currentIndex);
|
|
695
702
|
}
|
|
703
|
+
}
|
|
696
704
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
this.items = list.map(entry =>
|
|
718
|
-
this.createVirtualItem(typeof entry === 'string' ? {src: entry} : entry)
|
|
719
|
-
);
|
|
720
|
-
|
|
721
|
-
this.currentIndex = Math.max(
|
|
722
|
-
0,
|
|
723
|
-
Math.min(opts.startIndex || 0, this.items.length - 1)
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
this._activateLightbox();
|
|
727
|
-
|
|
728
|
-
// Trigger callback
|
|
729
|
-
if (typeof this.options.onOpen === 'function') {
|
|
730
|
-
this.options.onOpen(this.items[this.currentIndex], this.currentIndex);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
705
|
+
/**
|
|
706
|
+
* Open lightbox programmatically with image(s) not present in the DOM.
|
|
707
|
+
* Accepts a single URL, an array of URLs, a config object, or an array
|
|
708
|
+
* of config objects — so all of these work:
|
|
709
|
+
*
|
|
710
|
+
* lightbox.openImage('photo.jpg');
|
|
711
|
+
* lightbox.openImage(['a.jpg', 'b.jpg'], { startIndex: 1 });
|
|
712
|
+
* lightbox.openImage({ src: 'photo.jpg', caption: 'Hello' });
|
|
713
|
+
* lightbox.openImage([
|
|
714
|
+
* { src: 'a.jpg', caption: 'First' },
|
|
715
|
+
* { src: 'https://youtu.be/xyz', type: 'video', thumb: 'poster.jpg' },
|
|
716
|
+
* ]);
|
|
717
|
+
*
|
|
718
|
+
* @param {string|string[]|Object|Object[]} images - URL(s) or config object(s)
|
|
719
|
+
* @param {Object} [opts] - Options
|
|
720
|
+
* @param {number} [opts.startIndex=0] - Index to open at when passing an array
|
|
721
|
+
*/
|
|
722
|
+
openImage(images, opts = {}) {
|
|
723
|
+
const list = Array.isArray(images) ? images : [images];
|
|
733
724
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
* any changes to getImageSource, getThumbnailSource, updateCaption, etc.
|
|
738
|
-
* @param {Object} config
|
|
739
|
-
* @param {string} config.src - Full-size image/video/iframe URL
|
|
740
|
-
* @param {string} [config.thumb] - Thumbnail URL (falls back to src)
|
|
741
|
-
* @param {string} [config.type='image'] - 'image', 'video', or 'iframe'
|
|
742
|
-
* @param {string} [config.caption=''] - Caption text
|
|
743
|
-
* @param {string} [config.alt=''] - Alt text for the thumbnail img
|
|
744
|
-
* @returns {Object} Virtual item
|
|
745
|
-
*/
|
|
746
|
-
createVirtualItem({src, thumb, type = 'image', caption = '', alt = ''}) {
|
|
747
|
-
const thumbSrc = thumb || src;
|
|
748
|
-
return {
|
|
749
|
-
dataset: {src, type, caption},
|
|
750
|
-
src: thumbSrc,
|
|
751
|
-
alt,
|
|
752
|
-
title: caption,
|
|
753
|
-
getAttribute: (name) => name === 'src' ? thumbSrc : null,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Shared activation sequence used by both open() and openImage().
|
|
759
|
-
* Resets state, shows the lightbox, and triggers all UI updates.
|
|
760
|
-
*/
|
|
761
|
-
_activateLightbox() {
|
|
762
|
-
// Reset states
|
|
763
|
-
this.isZoomed = false;
|
|
764
|
-
this.isFullscreen = false;
|
|
765
|
-
this.thumbnailsVisible = true;
|
|
766
|
-
this.currentX = 0;
|
|
767
|
-
this.currentY = 0;
|
|
768
|
-
this.currentScale = 1;
|
|
725
|
+
this.items = list.map(entry =>
|
|
726
|
+
this.createVirtualItem(typeof entry === 'string' ? {src: entry} : entry)
|
|
727
|
+
);
|
|
769
728
|
|
|
770
|
-
|
|
771
|
-
|
|
729
|
+
this.currentIndex = Math.max(
|
|
730
|
+
0,
|
|
731
|
+
Math.min(opts.startIndex || 0, this.items.length - 1)
|
|
732
|
+
);
|
|
772
733
|
|
|
773
|
-
|
|
774
|
-
this.updateThumbnails();
|
|
775
|
-
this.updateCounter();
|
|
776
|
-
this.updateNavigation();
|
|
777
|
-
this.updateCaption();
|
|
778
|
-
this.updateButtons();
|
|
734
|
+
this._activateLightbox();
|
|
779
735
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
} else {
|
|
784
|
-
this.showControls();
|
|
785
|
-
}
|
|
736
|
+
// Trigger callback
|
|
737
|
+
if (typeof this.options.onOpen === 'function') {
|
|
738
|
+
this.options.onOpen(this.items[this.currentIndex], this.currentIndex);
|
|
786
739
|
}
|
|
740
|
+
}
|
|
787
741
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
742
|
+
/**
|
|
743
|
+
* Create a virtual item that mimics a DOM element's interface so
|
|
744
|
+
* programmatic images work through the existing render pipeline without
|
|
745
|
+
* any changes to getImageSource, getThumbnailSource, updateCaption, etc.
|
|
746
|
+
* @param {Object} config
|
|
747
|
+
* @param {string} config.src - Full-size image/video/iframe URL
|
|
748
|
+
* @param {string} [config.thumb] - Thumbnail URL (falls back to src)
|
|
749
|
+
* @param {string} [config.type='image'] - 'image', 'video', or 'iframe'
|
|
750
|
+
* @param {string} [config.caption=''] - Caption text
|
|
751
|
+
* @param {string} [config.alt=''] - Alt text for the thumbnail img
|
|
752
|
+
* @returns {Object} Virtual item
|
|
753
|
+
*/
|
|
754
|
+
createVirtualItem({src, thumb, type = 'image', caption = '', alt = ''}) {
|
|
755
|
+
const thumbSrc = thumb || src;
|
|
756
|
+
return {
|
|
757
|
+
dataset: {src, type, caption},
|
|
758
|
+
src: thumbSrc,
|
|
759
|
+
alt,
|
|
760
|
+
title: caption,
|
|
761
|
+
getAttribute: (name) => name === 'src' ? thumbSrc : null,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
803
764
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
765
|
+
/**
|
|
766
|
+
* Shared activation sequence used by both open() and openImage().
|
|
767
|
+
* Resets state, shows the lightbox, and triggers all UI updates.
|
|
768
|
+
*/
|
|
769
|
+
_activateLightbox() {
|
|
770
|
+
// Reset states
|
|
771
|
+
this.isZoomed = false;
|
|
772
|
+
this.isFullscreen = false;
|
|
773
|
+
this.thumbnailsVisible = true;
|
|
774
|
+
this.currentX = 0;
|
|
775
|
+
this.currentY = 0;
|
|
776
|
+
this.currentScale = 1;
|
|
777
|
+
|
|
778
|
+
this.zoomora.classList.add('active');
|
|
779
|
+
document.body.style.overflow = 'hidden';
|
|
780
|
+
|
|
781
|
+
this.loadContent();
|
|
782
|
+
this.updateThumbnails();
|
|
783
|
+
this.updateCounter();
|
|
784
|
+
this.updateNavigation();
|
|
785
|
+
this.updateCaption();
|
|
786
|
+
this.updateButtons();
|
|
787
|
+
|
|
788
|
+
// Initialize auto-hide
|
|
789
|
+
if (this.autoHideEnabled) {
|
|
790
|
+
this.startAutoHideTimer();
|
|
791
|
+
} else {
|
|
792
|
+
this.showControls();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
810
795
|
|
|
811
|
-
|
|
812
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Close lightbox
|
|
798
|
+
*/
|
|
799
|
+
close() {
|
|
800
|
+
if (this.isFullscreen) {
|
|
801
|
+
this.exitFullscreen();
|
|
802
|
+
}
|
|
813
803
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
this.showControls();
|
|
804
|
+
this.zoomora.classList.remove('active');
|
|
805
|
+
document.body.style.overflow = '';
|
|
817
806
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
807
|
+
// Stop playing media; the next open builds fresh media elements
|
|
808
|
+
const activeVideo = this.content.querySelector('video');
|
|
809
|
+
if (activeVideo) {
|
|
810
|
+
activeVideo.pause();
|
|
822
811
|
}
|
|
823
812
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (this.currentIndex > 0) {
|
|
829
|
-
this.currentIndex--;
|
|
830
|
-
this.loadContent();
|
|
831
|
-
this.updateCounter();
|
|
832
|
-
this.updateThumbnails();
|
|
833
|
-
this.updateNavigation();
|
|
834
|
-
this.updateCaption();
|
|
813
|
+
const activeIframe = this.content.querySelector('iframe');
|
|
814
|
+
if (activeIframe) {
|
|
815
|
+
activeIframe.src = 'about:blank';
|
|
816
|
+
}
|
|
835
817
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
818
|
+
this.isZoomed = false;
|
|
819
|
+
this.isFullscreen = false;
|
|
820
|
+
this.currentX = 0;
|
|
821
|
+
this.currentY = 0;
|
|
822
|
+
this.currentScale = 1;
|
|
823
|
+
|
|
824
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
825
|
+
if (media) {
|
|
826
|
+
media.classList.remove('zoomed');
|
|
827
|
+
media.style.transform = '';
|
|
828
|
+
media.style.cursor = '';
|
|
841
829
|
}
|
|
842
830
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
*/
|
|
846
|
-
next() {
|
|
847
|
-
if (this.currentIndex < this.items.length - 1) {
|
|
848
|
-
this.currentIndex++;
|
|
849
|
-
this.loadContent();
|
|
850
|
-
this.updateCounter();
|
|
851
|
-
this.updateThumbnails();
|
|
852
|
-
this.updateNavigation();
|
|
853
|
-
this.updateCaption();
|
|
831
|
+
this.zoomora.classList.remove('fullscreen');
|
|
832
|
+
this.updateButtons();
|
|
854
833
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
834
|
+
// Clean up auto-hide
|
|
835
|
+
this.stopAutoHideTimer();
|
|
836
|
+
this.showControls();
|
|
837
|
+
|
|
838
|
+
// Trigger callback
|
|
839
|
+
if (typeof this.options.onClose === 'function') {
|
|
840
|
+
this.options.onClose(this.currentIndex);
|
|
860
841
|
}
|
|
842
|
+
}
|
|
861
843
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
844
|
+
/**
|
|
845
|
+
* Navigate to previous item
|
|
846
|
+
*/
|
|
847
|
+
prev() {
|
|
848
|
+
if (this.currentIndex > 0) {
|
|
849
|
+
this.currentIndex--;
|
|
850
|
+
this.loadContent();
|
|
851
|
+
this.updateCounter();
|
|
852
|
+
this.updateThumbnails();
|
|
853
|
+
this.updateNavigation();
|
|
854
|
+
this.updateCaption();
|
|
867
855
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
856
|
+
// Trigger callback
|
|
857
|
+
if (typeof this.options.onPrev === 'function') {
|
|
858
|
+
this.options.onPrev(this.currentIndex);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
871
862
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
863
|
+
/**
|
|
864
|
+
* Navigate to next item
|
|
865
|
+
*/
|
|
866
|
+
next() {
|
|
867
|
+
if (this.currentIndex < this.items.length - 1) {
|
|
868
|
+
this.currentIndex++;
|
|
869
|
+
this.loadContent();
|
|
870
|
+
this.updateCounter();
|
|
871
|
+
this.updateThumbnails();
|
|
872
|
+
this.updateNavigation();
|
|
873
|
+
this.updateCaption();
|
|
874
|
+
|
|
875
|
+
// Trigger callback
|
|
876
|
+
if (typeof this.options.onNext === 'function') {
|
|
877
|
+
this.options.onNext(this.currentIndex);
|
|
875
878
|
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
876
881
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
+
/**
|
|
883
|
+
* Load content for current item
|
|
884
|
+
*/
|
|
885
|
+
loadContent() {
|
|
886
|
+
if (!this.items[this.currentIndex]) return;
|
|
882
887
|
|
|
883
|
-
|
|
884
|
-
|
|
888
|
+
const item = this.items[this.currentIndex];
|
|
889
|
+
const src = this.getImageSource(item);
|
|
890
|
+
const type = item.dataset.type || 'image';
|
|
885
891
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
892
|
+
if (!src) {
|
|
893
|
+
console.error('Zoomora: No source found for item');
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Reset zoom state
|
|
898
|
+
this.isZoomed = false;
|
|
899
|
+
this.currentX = 0;
|
|
900
|
+
this.currentY = 0;
|
|
901
|
+
this.currentScale = 1;
|
|
902
|
+
|
|
903
|
+
// Show loading spinner
|
|
904
|
+
this.loading.style.display = 'block';
|
|
905
|
+
|
|
906
|
+
if (this.slidesContainer) {
|
|
907
|
+
this.slidesContainer.innerHTML = '';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
setTimeout(() => {
|
|
911
|
+
this.renderContent(src, type, item);
|
|
912
|
+
}, 100);
|
|
913
|
+
}
|
|
889
914
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
mediaElement.
|
|
923
|
-
mediaElement.setAttribute('preload', 'metadata');
|
|
924
|
-
|
|
925
|
-
const posterSrc = item.src || item.getAttribute('src');
|
|
926
|
-
if (posterSrc) {
|
|
927
|
-
mediaElement.setAttribute('poster', posterSrc);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const playButton = document.createElement('div');
|
|
931
|
-
playButton.className = 'zoomora-play-button';
|
|
932
|
-
playButton.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
|
|
933
|
-
|
|
934
|
-
playButton.addEventListener('click', () => {
|
|
935
|
-
mediaElement.play();
|
|
936
|
-
playButton.style.display = 'none';
|
|
937
|
-
mediaElement.setAttribute('controls', '');
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
mediaElement.addEventListener('ended', () => {
|
|
941
|
-
playButton.style.display = 'flex';
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
videoContainer.appendChild(mediaElement);
|
|
945
|
-
videoContainer.appendChild(playButton);
|
|
946
|
-
slideContainer.appendChild(videoContainer);
|
|
947
|
-
} else {
|
|
948
|
-
mediaElement = document.createElement('iframe');
|
|
949
|
-
mediaElement.src = src;
|
|
950
|
-
mediaElement.className = 'zoomora-video';
|
|
951
|
-
mediaElement.setAttribute('allowfullscreen', '');
|
|
952
|
-
mediaElement.setAttribute('frameborder', '0');
|
|
953
|
-
slideContainer.appendChild(mediaElement);
|
|
915
|
+
/**
|
|
916
|
+
* Render content based on type
|
|
917
|
+
* @param {string} src - Source URL
|
|
918
|
+
* @param {string} type - Content type
|
|
919
|
+
* @param {Element} item - Original item element
|
|
920
|
+
*/
|
|
921
|
+
renderContent(src, type, item) {
|
|
922
|
+
const slideContainer = document.createElement('div');
|
|
923
|
+
slideContainer.className = 'zoomora-slide-container';
|
|
924
|
+
|
|
925
|
+
// Create slide wrapper
|
|
926
|
+
const slide = document.createElement('div');
|
|
927
|
+
slide.className = `zoomora-slide ${this.options.transition === 'slide' ? 'slide-effect' : 'fade-effect'}`;
|
|
928
|
+
slide.style.opacity = '0';
|
|
929
|
+
slide.style.transform = this.options.transition === 'slide' ? 'translateX(100%)' : 'scale(0.8)';
|
|
930
|
+
|
|
931
|
+
let mediaElement;
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
switch (type) {
|
|
935
|
+
case 'video':
|
|
936
|
+
if (src.endsWith('.mp4') || src.endsWith('.webm') || src.endsWith('.ogg')) {
|
|
937
|
+
const videoContainer = document.createElement('div');
|
|
938
|
+
videoContainer.className = 'zoomora-video-container';
|
|
939
|
+
|
|
940
|
+
mediaElement = document.createElement('video');
|
|
941
|
+
mediaElement.src = src;
|
|
942
|
+
mediaElement.className = 'zoomora-video';
|
|
943
|
+
mediaElement.setAttribute('preload', 'metadata');
|
|
944
|
+
|
|
945
|
+
const posterSrc = item.src || item.getAttribute('src');
|
|
946
|
+
if (posterSrc) {
|
|
947
|
+
mediaElement.setAttribute('poster', posterSrc);
|
|
954
948
|
}
|
|
955
|
-
break;
|
|
956
949
|
|
|
957
|
-
|
|
950
|
+
const playButton = document.createElement('div');
|
|
951
|
+
playButton.className = 'zoomora-play-button';
|
|
952
|
+
playButton.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
|
|
953
|
+
|
|
954
|
+
playButton.addEventListener('click', () => {
|
|
955
|
+
mediaElement.play();
|
|
956
|
+
playButton.style.display = 'none';
|
|
957
|
+
mediaElement.setAttribute('controls', '');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
mediaElement.addEventListener('ended', () => {
|
|
961
|
+
playButton.style.display = 'flex';
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
videoContainer.appendChild(mediaElement);
|
|
965
|
+
videoContainer.appendChild(playButton);
|
|
966
|
+
if (this.options.autoplayVideos) {
|
|
967
|
+
mediaElement.play()
|
|
968
|
+
.then(() => {
|
|
969
|
+
playButton.style.display = 'none';
|
|
970
|
+
mediaElement.setAttribute('controls', '');
|
|
971
|
+
})
|
|
972
|
+
.catch(() => {
|
|
973
|
+
// Autoplay blocked by the browser: the play button stays visible
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
slideContainer.appendChild(videoContainer);
|
|
977
|
+
} else {
|
|
958
978
|
mediaElement = document.createElement('iframe');
|
|
959
979
|
mediaElement.src = src;
|
|
960
|
-
mediaElement.className = 'zoomora-
|
|
980
|
+
mediaElement.className = 'zoomora-video';
|
|
981
|
+
mediaElement.setAttribute('allowfullscreen', '');
|
|
982
|
+
mediaElement.setAttribute('allow', 'autoplay; fullscreen; picture-in-picture');
|
|
961
983
|
mediaElement.setAttribute('frameborder', '0');
|
|
962
984
|
slideContainer.appendChild(mediaElement);
|
|
963
|
-
|
|
985
|
+
}
|
|
986
|
+
break;
|
|
964
987
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
988
|
+
case 'iframe':
|
|
989
|
+
mediaElement = document.createElement('iframe');
|
|
990
|
+
mediaElement.src = src;
|
|
991
|
+
mediaElement.className = 'zoomora-iframe';
|
|
992
|
+
mediaElement.setAttribute('frameborder', '0');
|
|
993
|
+
slideContainer.appendChild(mediaElement);
|
|
994
|
+
break;
|
|
970
995
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
996
|
+
default:
|
|
997
|
+
mediaElement = document.createElement('img');
|
|
998
|
+
mediaElement.src = src;
|
|
999
|
+
mediaElement.className = 'zoomora-media';
|
|
1000
|
+
mediaElement.alt = item.alt || item.title || '';
|
|
976
1001
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1002
|
+
mediaElement.onload = () => {
|
|
1003
|
+
this.loading.style.display = 'none';
|
|
1004
|
+
this.checkZoomability(mediaElement);
|
|
1005
|
+
this.animateSlideIn(slide);
|
|
1006
|
+
};
|
|
982
1007
|
|
|
983
|
-
|
|
984
|
-
|
|
1008
|
+
mediaElement.onerror = () => {
|
|
1009
|
+
this.loading.style.display = 'none';
|
|
1010
|
+
console.error('Zoomora: Failed to load image:', src);
|
|
1011
|
+
this.animateSlideIn(slide);
|
|
1012
|
+
};
|
|
985
1013
|
|
|
986
|
-
|
|
1014
|
+
slideContainer.appendChild(mediaElement);
|
|
1015
|
+
}
|
|
987
1016
|
|
|
988
|
-
|
|
989
|
-
this.slidesContainer.innerHTML = '';
|
|
990
|
-
this.slidesContainer.appendChild(slide);
|
|
1017
|
+
slide.appendChild(slideContainer);
|
|
991
1018
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
1019
|
+
// Clear existing slides and add new one
|
|
1020
|
+
this.slidesContainer.innerHTML = '';
|
|
1021
|
+
this.slidesContainer.appendChild(slide);
|
|
996
1022
|
|
|
997
|
-
|
|
998
|
-
console.error('Zoomora: Error rendering content:', error);
|
|
1023
|
+
if (type !== 'image') {
|
|
999
1024
|
this.loading.style.display = 'none';
|
|
1025
|
+
this.animateSlideIn(slide);
|
|
1000
1026
|
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Animation helper
|
|
1004
|
-
animateSlideIn(slide) {
|
|
1005
|
-
// Force reflow
|
|
1006
|
-
slide.offsetHeight;
|
|
1007
|
-
|
|
1008
|
-
// Add active class and animate
|
|
1009
|
-
slide.classList.add('active');
|
|
1010
1027
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
slide.style.opacity = '1';
|
|
1015
|
-
} else {
|
|
1016
|
-
slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
|
|
1017
|
-
slide.style.transform = 'scale(1)';
|
|
1018
|
-
slide.style.opacity = '1';
|
|
1019
|
-
}
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
console.error('Zoomora: Error rendering content:', error);
|
|
1030
|
+
this.loading.style.display = 'none';
|
|
1020
1031
|
}
|
|
1032
|
+
}
|
|
1021
1033
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1034
|
+
// Animation helper
|
|
1035
|
+
animateSlideIn(slide) {
|
|
1036
|
+
// Force reflow
|
|
1037
|
+
slide.offsetHeight;
|
|
1038
|
+
|
|
1039
|
+
// Add active class and animate
|
|
1040
|
+
slide.classList.add('active');
|
|
1041
|
+
|
|
1042
|
+
if (this.options.transition === 'slide') {
|
|
1043
|
+
slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
|
|
1044
|
+
slide.style.transform = 'translateX(0)';
|
|
1045
|
+
slide.style.opacity = '1';
|
|
1046
|
+
} else {
|
|
1047
|
+
slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
|
|
1048
|
+
slide.style.transform = 'scale(1)';
|
|
1049
|
+
slide.style.opacity = '1';
|
|
1037
1050
|
}
|
|
1051
|
+
}
|
|
1038
1052
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1053
|
+
// Toggle auto-hide controls
|
|
1054
|
+
toggleAutoHide() {
|
|
1055
|
+
this.autoHideEnabled = !this.autoHideEnabled;
|
|
1056
|
+
const autoHideBtn = document.getElementById('zoomoraAutoHideBtn');
|
|
1042
1057
|
|
|
1043
|
-
|
|
1058
|
+
if (this.autoHideEnabled) {
|
|
1059
|
+
autoHideBtn.classList.add('active');
|
|
1060
|
+
autoHideBtn.title = 'Disable Auto-Hide Controls';
|
|
1044
1061
|
this.startAutoHideTimer();
|
|
1062
|
+
} else {
|
|
1063
|
+
autoHideBtn.classList.remove('active');
|
|
1064
|
+
autoHideBtn.title = 'Enable Auto-Hide Controls';
|
|
1065
|
+
this.stopAutoHideTimer();
|
|
1066
|
+
this.showControls();
|
|
1045
1067
|
}
|
|
1068
|
+
}
|
|
1046
1069
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1070
|
+
// Handle user activity
|
|
1071
|
+
handleUserActivity() {
|
|
1072
|
+
if (!this.autoHideEnabled) return;
|
|
1050
1073
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1074
|
+
this.showControls();
|
|
1075
|
+
this.startAutoHideTimer();
|
|
1076
|
+
}
|
|
1055
1077
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
clearTimeout(this.autoHideTimeout);
|
|
1060
|
-
this.autoHideTimeout = null;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1078
|
+
// Start auto-hide timer
|
|
1079
|
+
startAutoHideTimer() {
|
|
1080
|
+
this.stopAutoHideTimer();
|
|
1063
1081
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1082
|
+
this.autoHideTimeout = setTimeout(() => {
|
|
1083
|
+
this.hideControls();
|
|
1084
|
+
}, this.options.autoHideDelay);
|
|
1085
|
+
}
|
|
1067
1086
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1087
|
+
// Stop auto-hide timer
|
|
1088
|
+
stopAutoHideTimer() {
|
|
1089
|
+
if (this.autoHideTimeout) {
|
|
1090
|
+
clearTimeout(this.autoHideTimeout);
|
|
1091
|
+
this.autoHideTimeout = null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1070
1094
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
this.header.style.opacity = '1';
|
|
1075
|
-
}
|
|
1095
|
+
// Show controls
|
|
1096
|
+
showControls() {
|
|
1097
|
+
if (this.isControlsVisible) return;
|
|
1076
1098
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
this.caption.style.opacity = '1';
|
|
1080
|
-
}
|
|
1099
|
+
this.isControlsVisible = true;
|
|
1100
|
+
this.zoomora.classList.remove('controls-hidden');
|
|
1081
1101
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1102
|
+
// Animate controls back in
|
|
1103
|
+
if (this.header) {
|
|
1104
|
+
this.header.style.transform = 'translateY(0)';
|
|
1105
|
+
this.header.style.opacity = '1';
|
|
1106
|
+
}
|
|
1086
1107
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1108
|
+
if (this.caption) {
|
|
1109
|
+
this.caption.style.transform = 'translateX(-50%) translateY(0)';
|
|
1110
|
+
this.caption.style.opacity = '1';
|
|
1111
|
+
}
|
|
1091
1112
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1113
|
+
if (this.thumbs) {
|
|
1114
|
+
this.thumbs.style.transform = 'translateX(-50%) translateY(0)';
|
|
1115
|
+
this.thumbs.style.opacity = '1';
|
|
1096
1116
|
}
|
|
1097
1117
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1118
|
+
if (this.prevBtn) {
|
|
1119
|
+
this.prevBtn.style.transform = 'translateX(0)';
|
|
1120
|
+
this.prevBtn.style.opacity = '1';
|
|
1121
|
+
}
|
|
1101
1122
|
|
|
1102
|
-
|
|
1103
|
-
this.
|
|
1123
|
+
if (this.nextBtn) {
|
|
1124
|
+
this.nextBtn.style.transform = 'translateX(0)';
|
|
1125
|
+
this.nextBtn.style.opacity = '1';
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1104
1128
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
this.header.style.opacity = '0';
|
|
1109
|
-
}
|
|
1129
|
+
// Hide controls
|
|
1130
|
+
hideControls() {
|
|
1131
|
+
if (!this.isControlsVisible) return;
|
|
1110
1132
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
this.caption.style.opacity = '0';
|
|
1114
|
-
}
|
|
1133
|
+
this.isControlsVisible = false;
|
|
1134
|
+
this.zoomora.classList.add('controls-hidden');
|
|
1115
1135
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1136
|
+
// Animate controls out
|
|
1137
|
+
if (this.header) {
|
|
1138
|
+
this.header.style.transform = 'translateY(-100%)';
|
|
1139
|
+
this.header.style.opacity = '0';
|
|
1140
|
+
}
|
|
1120
1141
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1142
|
+
if (this.caption) {
|
|
1143
|
+
this.caption.style.transform = 'translateX(-50%) translateY(100%)';
|
|
1144
|
+
this.caption.style.opacity = '0';
|
|
1145
|
+
}
|
|
1125
1146
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1147
|
+
if (this.thumbs) {
|
|
1148
|
+
this.thumbs.style.transform = 'translateX(-50%) translateY(100%)';
|
|
1149
|
+
this.thumbs.style.opacity = '0';
|
|
1130
1150
|
}
|
|
1131
1151
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
*/
|
|
1136
|
-
checkZoomability(img) {
|
|
1137
|
-
if (!img || !img.naturalWidth || !img.naturalHeight) return;
|
|
1138
|
-
|
|
1139
|
-
const imgRect = img.getBoundingClientRect();
|
|
1140
|
-
const renderedWidth = imgRect.width;
|
|
1141
|
-
const renderedHeight = imgRect.height;
|
|
1142
|
-
const naturalWidth = img.naturalWidth;
|
|
1143
|
-
const naturalHeight = img.naturalHeight;
|
|
1144
|
-
|
|
1145
|
-
const isScaledDown = naturalWidth > renderedWidth || naturalHeight > renderedHeight;
|
|
1146
|
-
const zoomRatioX = naturalWidth / renderedWidth;
|
|
1147
|
-
const zoomRatioY = naturalHeight / renderedHeight;
|
|
1148
|
-
const maxZoomRatio = Math.max(zoomRatioX, zoomRatioY);
|
|
1149
|
-
|
|
1150
|
-
// Store zoom info
|
|
1151
|
-
img.dataset.zoomRatio = maxZoomRatio.toString();
|
|
1152
|
-
img.dataset.naturalWidth = naturalWidth.toString();
|
|
1153
|
-
img.dataset.naturalHeight = naturalHeight.toString();
|
|
1154
|
-
img.dataset.renderedWidth = renderedWidth.toString();
|
|
1155
|
-
img.dataset.renderedHeight = renderedHeight.toString();
|
|
1156
|
-
|
|
1157
|
-
// Enable zoom if significantly scaled down
|
|
1158
|
-
if (isScaledDown && maxZoomRatio > 1.1) {
|
|
1159
|
-
img.classList.remove('no-zoom');
|
|
1160
|
-
} else {
|
|
1161
|
-
img.classList.add('no-zoom');
|
|
1162
|
-
}
|
|
1152
|
+
if (this.prevBtn) {
|
|
1153
|
+
this.prevBtn.style.transform = 'translateX(-100%)';
|
|
1154
|
+
this.prevBtn.style.opacity = '0';
|
|
1163
1155
|
}
|
|
1164
1156
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
calculateZoomLevels(img) {
|
|
1171
|
-
if (!img || !img.naturalWidth || !img.naturalHeight) {
|
|
1172
|
-
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1173
|
-
}
|
|
1157
|
+
if (this.nextBtn) {
|
|
1158
|
+
this.nextBtn.style.transform = 'translateX(100%)';
|
|
1159
|
+
this.nextBtn.style.opacity = '0';
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1174
1162
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if image can be zoomed
|
|
1165
|
+
* @param {HTMLImageElement} img - Image element
|
|
1166
|
+
*/
|
|
1167
|
+
checkZoomability(img) {
|
|
1168
|
+
if (!img || !img.naturalWidth || !img.naturalHeight) return;
|
|
1169
|
+
|
|
1170
|
+
const imgRect = img.getBoundingClientRect();
|
|
1171
|
+
const renderedWidth = imgRect.width;
|
|
1172
|
+
const renderedHeight = imgRect.height;
|
|
1173
|
+
const naturalWidth = img.naturalWidth;
|
|
1174
|
+
const naturalHeight = img.naturalHeight;
|
|
1175
|
+
|
|
1176
|
+
const isScaledDown = naturalWidth > renderedWidth || naturalHeight > renderedHeight;
|
|
1177
|
+
const zoomRatioX = naturalWidth / renderedWidth;
|
|
1178
|
+
const zoomRatioY = naturalHeight / renderedHeight;
|
|
1179
|
+
const maxZoomRatio = Math.max(zoomRatioX, zoomRatioY);
|
|
1180
|
+
|
|
1181
|
+
// Store zoom info
|
|
1182
|
+
img.dataset.zoomRatio = maxZoomRatio.toString();
|
|
1183
|
+
img.dataset.naturalWidth = naturalWidth.toString();
|
|
1184
|
+
img.dataset.naturalHeight = naturalHeight.toString();
|
|
1185
|
+
img.dataset.renderedWidth = renderedWidth.toString();
|
|
1186
|
+
img.dataset.renderedHeight = renderedHeight.toString();
|
|
1187
|
+
|
|
1188
|
+
// Enable zoom if significantly scaled down
|
|
1189
|
+
if (isScaledDown && maxZoomRatio > 1.1) {
|
|
1190
|
+
img.classList.remove('no-zoom');
|
|
1191
|
+
} else {
|
|
1192
|
+
img.classList.add('no-zoom');
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1179
1195
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1196
|
+
/**
|
|
1197
|
+
* Calculate zoom levels for an image
|
|
1198
|
+
* @param {HTMLImageElement} img - Image element
|
|
1199
|
+
* @returns {Object} Zoom configuration
|
|
1200
|
+
*/
|
|
1201
|
+
calculateZoomLevels(img) {
|
|
1202
|
+
if (!img || !img.naturalWidth || !img.naturalHeight) {
|
|
1203
|
+
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1204
|
+
}
|
|
1182
1205
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1206
|
+
// Get CONTAINER size (not current displayed image size)
|
|
1207
|
+
const container = this.content.getBoundingClientRect();
|
|
1208
|
+
const containerWidth = container.width;
|
|
1209
|
+
const containerHeight = container.height;
|
|
1187
1210
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1191
|
-
}
|
|
1211
|
+
const naturalWidth = img.naturalWidth;
|
|
1212
|
+
const naturalHeight = img.naturalHeight;
|
|
1192
1213
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
: 3;
|
|
1214
|
+
// Calculate what the ORIGINAL fit-to-screen scale would be
|
|
1215
|
+
const scaleToFitWidth = containerWidth / naturalWidth;
|
|
1216
|
+
const scaleToFitHeight = containerHeight / naturalHeight;
|
|
1217
|
+
const fitScale = Math.min(scaleToFitWidth, scaleToFitHeight);
|
|
1198
1218
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1219
|
+
// If image naturally fits or is smaller than container, can't zoom
|
|
1220
|
+
if (fitScale >= 0.95) {
|
|
1221
|
+
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1222
|
+
}
|
|
1201
1223
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
});
|
|
1224
|
+
// Respect the configured ceiling (falls back to 3x only if misconfigured)
|
|
1225
|
+
const configuredMax = Number(this.options.maxZoomScale);
|
|
1226
|
+
const maxZoom = Number.isFinite(configuredMax) && configuredMax > 0
|
|
1227
|
+
? Math.max(1, configuredMax)
|
|
1228
|
+
: 3;
|
|
1208
1229
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (actualSizeScale > levels[levels.length - 1]) {
|
|
1212
|
-
levels.push(actualSizeScale);
|
|
1213
|
-
}
|
|
1230
|
+
// Calculate zoom levels based on FIT scale (not current scale)
|
|
1231
|
+
const levels = [1]; // Start with fit-to-screen
|
|
1214
1232
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
baseScale: fitScale,
|
|
1220
|
-
naturalWidth: naturalWidth,
|
|
1221
|
-
naturalHeight: naturalHeight
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
/**
|
|
1226
|
-
* Update counter display
|
|
1227
|
-
*/
|
|
1228
|
-
updateCounter() {
|
|
1229
|
-
if (this.options.showCounter && this.counter) {
|
|
1230
|
-
this.counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
|
|
1233
|
+
// Add progressive levels up to the configured max: 1.5x, 2x, 3x, 4x, 5x...
|
|
1234
|
+
[1.5, 2, 3, 4, 5].forEach((candidate) => {
|
|
1235
|
+
if (candidate <= maxZoom && fitScale * candidate <= 1) {
|
|
1236
|
+
levels.push(candidate);
|
|
1231
1237
|
}
|
|
1232
|
-
}
|
|
1238
|
+
});
|
|
1233
1239
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1240
|
+
// Add actual size (100%) as final level, capped at the configured max
|
|
1241
|
+
const actualSizeScale = Math.min(1 / fitScale, maxZoom);
|
|
1242
|
+
if (actualSizeScale > levels[levels.length - 1]) {
|
|
1243
|
+
levels.push(actualSizeScale);
|
|
1244
|
+
}
|
|
1239
1245
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1246
|
+
return {
|
|
1247
|
+
canZoom: levels.length > 1,
|
|
1248
|
+
levels: levels,
|
|
1249
|
+
currentLevel: 0,
|
|
1250
|
+
baseScale: fitScale,
|
|
1251
|
+
naturalWidth: naturalWidth,
|
|
1252
|
+
naturalHeight: naturalHeight
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1242
1255
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
} else {
|
|
1250
|
-
this.caption.style.transform = 'translateX(-50%) translateY(100%)';
|
|
1251
|
-
this.caption.style.opacity = '0';
|
|
1252
|
-
this.caption.style.pointerEvents = 'none';
|
|
1253
|
-
this.caption.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
1254
|
-
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Update counter display
|
|
1258
|
+
*/
|
|
1259
|
+
updateCounter() {
|
|
1260
|
+
if (this.options.showCounter && this.counter) {
|
|
1261
|
+
this.counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
|
|
1255
1262
|
}
|
|
1263
|
+
}
|
|
1256
1264
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* Update caption display
|
|
1267
|
+
*/
|
|
1268
|
+
updateCaption() {
|
|
1269
|
+
if (!this.caption || !this.items[this.currentIndex]) return;
|
|
1270
|
+
|
|
1271
|
+
const item = this.items[this.currentIndex];
|
|
1272
|
+
const caption = item.dataset.caption || item.title || '';
|
|
1273
|
+
|
|
1274
|
+
if (caption) {
|
|
1275
|
+
this.caption.textContent = caption;
|
|
1276
|
+
this.caption.style.transform = 'translateX(-50%) translateY(0)';
|
|
1277
|
+
this.caption.style.opacity = '1';
|
|
1278
|
+
this.caption.style.pointerEvents = 'auto';
|
|
1279
|
+
this.caption.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
1280
|
+
} else {
|
|
1281
|
+
this.caption.style.transform = 'translateX(-50%) translateY(100%)';
|
|
1282
|
+
this.caption.style.opacity = '0';
|
|
1283
|
+
this.caption.style.pointerEvents = 'none';
|
|
1284
|
+
this.caption.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1262
1287
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1288
|
+
/**
|
|
1289
|
+
* Update thumbnails display
|
|
1290
|
+
*/
|
|
1291
|
+
updateThumbnails() {
|
|
1292
|
+
if (!this.options.showThumbnails || !this.thumbs) return;
|
|
1268
1293
|
|
|
1269
|
-
|
|
1294
|
+
if (this.items.length === 1) {
|
|
1295
|
+
this.caption.classList.add('single-item-thumbnail');
|
|
1296
|
+
this.thumbs.style.display = 'none';
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1270
1299
|
|
|
1271
|
-
|
|
1272
|
-
this.thumbs.innerHTML = '';
|
|
1300
|
+
this.caption.classList.remove('single-item-thumbnail');
|
|
1273
1301
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
thumb.className = 'zoomora-thumb';
|
|
1277
|
-
if (index === this.currentIndex) thumb.classList.add('active');
|
|
1302
|
+
this.thumbs.style.display = 'flex';
|
|
1303
|
+
this.thumbs.innerHTML = '';
|
|
1278
1304
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1305
|
+
this.items.forEach((item, index) => {
|
|
1306
|
+
const thumb = document.createElement('div');
|
|
1307
|
+
thumb.className = 'zoomora-thumb';
|
|
1308
|
+
if (index === this.currentIndex) thumb.classList.add('active');
|
|
1281
1309
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
img.alt = item.alt || item.title || '';
|
|
1285
|
-
img.onerror = () => {
|
|
1286
|
-
thumb.style.display = 'none';
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1310
|
+
const img = document.createElement('img');
|
|
1311
|
+
const thumbSrc = this.getThumbnailSource(item);
|
|
1289
1312
|
|
|
1290
|
-
|
|
1313
|
+
if (thumbSrc) {
|
|
1314
|
+
img.src = thumbSrc;
|
|
1315
|
+
img.alt = item.alt || item.title || '';
|
|
1316
|
+
img.onerror = () => {
|
|
1317
|
+
thumb.style.display = 'none';
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1291
1320
|
|
|
1292
|
-
|
|
1293
|
-
this.currentIndex = index;
|
|
1294
|
-
this.loadContent();
|
|
1295
|
-
this.updateCounter();
|
|
1296
|
-
this.updateThumbnails();
|
|
1297
|
-
this.updateNavigation();
|
|
1298
|
-
this.updateCaption();
|
|
1299
|
-
});
|
|
1321
|
+
thumb.appendChild(img);
|
|
1300
1322
|
|
|
1301
|
-
|
|
1323
|
+
thumb.addEventListener('click', () => {
|
|
1324
|
+
this.currentIndex = index;
|
|
1325
|
+
this.loadContent();
|
|
1326
|
+
this.updateCounter();
|
|
1327
|
+
this.updateThumbnails();
|
|
1328
|
+
this.updateNavigation();
|
|
1329
|
+
this.updateCaption();
|
|
1302
1330
|
});
|
|
1303
|
-
}
|
|
1304
1331
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
updateNavigation() {
|
|
1309
|
-
const prevBtn = document.getElementById('zoomoraPrevBtn');
|
|
1310
|
-
const nextBtn = document.getElementById('zoomoraNextBtn');
|
|
1332
|
+
this.thumbs.appendChild(thumb);
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1311
1335
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1336
|
+
/**
|
|
1337
|
+
* Update navigation buttons
|
|
1338
|
+
*/
|
|
1339
|
+
updateNavigation() {
|
|
1340
|
+
const prevBtn = document.getElementById('zoomoraPrevBtn');
|
|
1341
|
+
const nextBtn = document.getElementById('zoomoraNextBtn');
|
|
1342
|
+
|
|
1343
|
+
if (prevBtn) {
|
|
1344
|
+
prevBtn.style.display = this.currentIndex > 0 ? 'flex' : 'none';
|
|
1345
|
+
}
|
|
1346
|
+
if (nextBtn) {
|
|
1347
|
+
nextBtn.style.display = this.currentIndex < this.items.length - 1 ? 'flex' : 'none';
|
|
1318
1348
|
}
|
|
1349
|
+
}
|
|
1319
1350
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* Update control buttons
|
|
1353
|
+
*/
|
|
1354
|
+
updateButtons() {
|
|
1355
|
+
const fullscreenBtn = document.getElementById('zoomoraFullscreenBtn');
|
|
1356
|
+
const collapseBtn = document.getElementById('zoomoraCollapseBtn');
|
|
1326
1357
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1358
|
+
if (fullscreenBtn && collapseBtn) {
|
|
1359
|
+
if (this.isFullscreen) {
|
|
1360
|
+
fullscreenBtn.style.display = 'none';
|
|
1361
|
+
collapseBtn.style.display = 'flex';
|
|
1362
|
+
} else {
|
|
1363
|
+
fullscreenBtn.style.display = 'flex';
|
|
1364
|
+
collapseBtn.style.display = 'none';
|
|
1335
1365
|
}
|
|
1336
1366
|
}
|
|
1367
|
+
}
|
|
1337
1368
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1369
|
+
/**
|
|
1370
|
+
* Toggle zoom state
|
|
1371
|
+
*/
|
|
1372
|
+
toggleZoom() {
|
|
1373
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
1374
|
+
if (!media || media.tagName !== 'IMG') return;
|
|
1344
1375
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1376
|
+
// Calculate zoom levels
|
|
1377
|
+
const zoomConfig = this.calculateZoomLevels(media);
|
|
1347
1378
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1379
|
+
if (!zoomConfig.canZoom) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1351
1382
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1383
|
+
// Get or initialize current zoom level
|
|
1384
|
+
if (!media.dataset.zoomLevel) {
|
|
1385
|
+
media.dataset.zoomLevel = '0';
|
|
1386
|
+
}
|
|
1356
1387
|
|
|
1357
|
-
|
|
1388
|
+
let currentLevel = parseInt(media.dataset.zoomLevel) || 0;
|
|
1358
1389
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1390
|
+
// Cycle to next level
|
|
1391
|
+
currentLevel++;
|
|
1392
|
+
if (currentLevel >= zoomConfig.levels.length) {
|
|
1393
|
+
// Reset to fit-to-screen
|
|
1394
|
+
currentLevel = 0;
|
|
1395
|
+
}
|
|
1365
1396
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
if (currentLevel === 0) {
|
|
1370
|
-
// Back to fit-to-screen
|
|
1371
|
-
this.isZoomed = false;
|
|
1372
|
-
this.isDragging = false;
|
|
1373
|
-
media.classList.remove('dragging');
|
|
1374
|
-
media.style.transform = '';
|
|
1375
|
-
media.style.cursor = 'pointer';
|
|
1376
|
-
media.classList.remove('zoomed');
|
|
1377
|
-
this.currentX = 0;
|
|
1378
|
-
this.currentY = 0;
|
|
1379
|
-
this.currentScale = 1;
|
|
1380
|
-
} else {
|
|
1381
|
-
// Zoom in - reset position to center
|
|
1382
|
-
this.isZoomed = true;
|
|
1383
|
-
this.currentScale = targetScale;
|
|
1384
|
-
this.currentX = 0; // Always reset to center on zoom change
|
|
1385
|
-
this.currentY = 0;
|
|
1397
|
+
const targetScale = zoomConfig.levels[currentLevel];
|
|
1398
|
+
media.dataset.zoomLevel = currentLevel.toString();
|
|
1386
1399
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1400
|
+
if (currentLevel === 0) {
|
|
1401
|
+
// Back to fit-to-screen
|
|
1402
|
+
this.isZoomed = false;
|
|
1403
|
+
this.isDragging = false;
|
|
1404
|
+
media.classList.remove('dragging');
|
|
1405
|
+
media.style.transform = '';
|
|
1406
|
+
media.style.cursor = 'pointer';
|
|
1407
|
+
media.classList.remove('zoomed');
|
|
1408
|
+
this.currentX = 0;
|
|
1409
|
+
this.currentY = 0;
|
|
1410
|
+
this.currentScale = 1;
|
|
1411
|
+
} else {
|
|
1412
|
+
// Zoom in - reset position to center
|
|
1413
|
+
this.isZoomed = true;
|
|
1414
|
+
this.currentScale = targetScale;
|
|
1415
|
+
this.currentX = 0; // Always reset to center on zoom change
|
|
1416
|
+
this.currentY = 0;
|
|
1392
1417
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
toggleFullscreen() {
|
|
1397
|
-
if (!this.isFullscreen) {
|
|
1398
|
-
this.enterFullscreen();
|
|
1399
|
-
} else {
|
|
1400
|
-
this.exitFullscreen();
|
|
1401
|
-
}
|
|
1418
|
+
media.style.transform = `translate(0px, 0px) scale(${targetScale})`;
|
|
1419
|
+
media.style.cursor = 'grab';
|
|
1420
|
+
media.classList.add('zoomed');
|
|
1402
1421
|
}
|
|
1422
|
+
}
|
|
1403
1423
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (element.requestFullscreen) {
|
|
1413
|
-
element.requestFullscreen()
|
|
1414
|
-
.then(() => {
|
|
1415
|
-
this.isFullscreen = true;
|
|
1416
|
-
this.zoomora.classList.add('fullscreen');
|
|
1417
|
-
this.updateButtons();
|
|
1418
|
-
})
|
|
1419
|
-
.catch(err => {
|
|
1420
|
-
console.warn('Zoomora: Fullscreen request failed:', err.message);
|
|
1421
|
-
this.fallbackFullscreen();
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
// Try webkit (Safari, older Chrome)
|
|
1425
|
-
else if (element.webkitRequestFullscreen) {
|
|
1426
|
-
element.webkitRequestFullscreen();
|
|
1427
|
-
// Webkit doesn't return a promise, so set state immediately
|
|
1428
|
-
this.isFullscreen = true;
|
|
1429
|
-
this.zoomora.classList.add('fullscreen');
|
|
1430
|
-
this.updateButtons();
|
|
1431
|
-
}
|
|
1432
|
-
// Try moz (Firefox)
|
|
1433
|
-
else if (element.mozRequestFullScreen) {
|
|
1434
|
-
element.mozRequestFullScreen();
|
|
1435
|
-
this.isFullscreen = true;
|
|
1436
|
-
this.zoomora.classList.add('fullscreen');
|
|
1437
|
-
this.updateButtons();
|
|
1438
|
-
}
|
|
1439
|
-
// Try ms (IE11)
|
|
1440
|
-
else if (element.msRequestFullscreen) {
|
|
1441
|
-
element.msRequestFullscreen();
|
|
1442
|
-
this.isFullscreen = true;
|
|
1443
|
-
this.zoomora.classList.add('fullscreen');
|
|
1444
|
-
this.updateButtons();
|
|
1445
|
-
}
|
|
1446
|
-
// Fallback to CSS fullscreen
|
|
1447
|
-
else {
|
|
1448
|
-
console.warn('Zoomora: Fullscreen API not supported, using CSS fallback');
|
|
1449
|
-
this.fallbackFullscreen();
|
|
1450
|
-
}
|
|
1451
|
-
} catch (error) {
|
|
1452
|
-
console.error('Zoomora: Error entering fullscreen:', error);
|
|
1453
|
-
this.fallbackFullscreen();
|
|
1454
|
-
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Toggle fullscreen mode
|
|
1426
|
+
*/
|
|
1427
|
+
toggleFullscreen() {
|
|
1428
|
+
if (!this.isFullscreen) {
|
|
1429
|
+
this.enterFullscreen();
|
|
1430
|
+
} else {
|
|
1431
|
+
this.exitFullscreen();
|
|
1455
1432
|
}
|
|
1433
|
+
}
|
|
1456
1434
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1435
|
+
/**
|
|
1436
|
+
* Enter fullscreen mode
|
|
1437
|
+
*/
|
|
1438
|
+
enterFullscreen() {
|
|
1439
|
+
const element = this.zoomora;
|
|
1440
|
+
|
|
1441
|
+
try {
|
|
1442
|
+
// Try standard API first
|
|
1443
|
+
if (element.requestFullscreen) {
|
|
1444
|
+
element.requestFullscreen()
|
|
1445
|
+
.then(() => {
|
|
1446
|
+
this.isFullscreen = true;
|
|
1447
|
+
this.zoomora.classList.add('fullscreen');
|
|
1448
|
+
this.updateButtons();
|
|
1449
|
+
})
|
|
1450
|
+
.catch(err => {
|
|
1451
|
+
console.warn('Zoomora: Fullscreen request failed:', err.message);
|
|
1452
|
+
this.fallbackFullscreen();
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
// Try webkit (Safari, older Chrome)
|
|
1456
|
+
else if (element.webkitRequestFullscreen) {
|
|
1457
|
+
element.webkitRequestFullscreen();
|
|
1458
|
+
// Webkit doesn't return a promise, so set state immediately
|
|
1459
|
+
this.isFullscreen = true;
|
|
1460
|
+
this.zoomora.classList.add('fullscreen');
|
|
1461
|
+
this.updateButtons();
|
|
1473
1462
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
this.isFullscreen = false;
|
|
1481
|
-
this.zoomora.classList.remove('fullscreen');
|
|
1482
|
-
this.updateButtons();
|
|
1483
|
-
})
|
|
1484
|
-
.catch(err => {
|
|
1485
|
-
console.warn('Zoomora: Exit fullscreen failed:', err.message);
|
|
1486
|
-
this.fallbackExitFullscreen();
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
// Try webkit
|
|
1490
|
-
else if (document.webkitExitFullscreen) {
|
|
1491
|
-
document.webkitExitFullscreen();
|
|
1492
|
-
this.isFullscreen = false;
|
|
1493
|
-
this.zoomora.classList.remove('fullscreen');
|
|
1494
|
-
this.updateButtons();
|
|
1495
|
-
}
|
|
1496
|
-
// Try moz
|
|
1497
|
-
else if (document.mozCancelFullScreen) {
|
|
1498
|
-
document.mozCancelFullScreen();
|
|
1499
|
-
this.isFullscreen = false;
|
|
1500
|
-
this.zoomora.classList.remove('fullscreen');
|
|
1501
|
-
this.updateButtons();
|
|
1502
|
-
}
|
|
1503
|
-
// Try ms
|
|
1504
|
-
else if (document.msExitFullscreen) {
|
|
1505
|
-
document.msExitFullscreen();
|
|
1506
|
-
this.isFullscreen = false;
|
|
1507
|
-
this.zoomora.classList.remove('fullscreen');
|
|
1508
|
-
this.updateButtons();
|
|
1509
|
-
}
|
|
1510
|
-
// Fallback
|
|
1511
|
-
else {
|
|
1512
|
-
this.fallbackExitFullscreen();
|
|
1513
|
-
}
|
|
1514
|
-
} catch (error) {
|
|
1515
|
-
console.error('Zoomora: Error exiting fullscreen:', error);
|
|
1516
|
-
this.fallbackExitFullscreen();
|
|
1463
|
+
// Try moz (Firefox)
|
|
1464
|
+
else if (element.mozRequestFullScreen) {
|
|
1465
|
+
element.mozRequestFullScreen();
|
|
1466
|
+
this.isFullscreen = true;
|
|
1467
|
+
this.zoomora.classList.add('fullscreen');
|
|
1468
|
+
this.updateButtons();
|
|
1517
1469
|
}
|
|
1470
|
+
// Try ms (IE11)
|
|
1471
|
+
else if (element.msRequestFullscreen) {
|
|
1472
|
+
element.msRequestFullscreen();
|
|
1473
|
+
this.isFullscreen = true;
|
|
1474
|
+
this.zoomora.classList.add('fullscreen');
|
|
1475
|
+
this.updateButtons();
|
|
1476
|
+
}
|
|
1477
|
+
// Fallback to CSS fullscreen
|
|
1478
|
+
else {
|
|
1479
|
+
console.warn('Zoomora: Fullscreen API not supported, using CSS fallback');
|
|
1480
|
+
this.fallbackFullscreen();
|
|
1481
|
+
}
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
console.error('Zoomora: Error entering fullscreen:', error);
|
|
1484
|
+
this.fallbackFullscreen();
|
|
1518
1485
|
}
|
|
1486
|
+
}
|
|
1519
1487
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1488
|
+
/**
|
|
1489
|
+
* Exit fullscreen mode
|
|
1490
|
+
*/
|
|
1491
|
+
exitFullscreen() {
|
|
1492
|
+
// First check if we're actually in fullscreen
|
|
1493
|
+
const isInFullscreen = !!(
|
|
1494
|
+
document.fullscreenElement ||
|
|
1495
|
+
document.webkitFullscreenElement ||
|
|
1496
|
+
document.mozFullScreenElement ||
|
|
1497
|
+
document.msFullscreenElement
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
if (!isInFullscreen) {
|
|
1501
|
+
// Not in real fullscreen, just exit CSS fullscreen
|
|
1502
|
+
this.fallbackExitFullscreen();
|
|
1503
|
+
return;
|
|
1527
1504
|
}
|
|
1528
1505
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1506
|
+
try {
|
|
1507
|
+
// Try standard API first
|
|
1508
|
+
if (document.exitFullscreen) {
|
|
1509
|
+
document.exitFullscreen()
|
|
1510
|
+
.then(() => {
|
|
1511
|
+
this.isFullscreen = false;
|
|
1512
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1513
|
+
this.updateButtons();
|
|
1514
|
+
})
|
|
1515
|
+
.catch(err => {
|
|
1516
|
+
console.warn('Zoomora: Exit fullscreen failed:', err.message);
|
|
1517
|
+
this.fallbackExitFullscreen();
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
// Try webkit
|
|
1521
|
+
else if (document.webkitExitFullscreen) {
|
|
1522
|
+
document.webkitExitFullscreen();
|
|
1523
|
+
this.isFullscreen = false;
|
|
1524
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1525
|
+
this.updateButtons();
|
|
1526
|
+
}
|
|
1527
|
+
// Try moz
|
|
1528
|
+
else if (document.mozCancelFullScreen) {
|
|
1529
|
+
document.mozCancelFullScreen();
|
|
1530
|
+
this.isFullscreen = false;
|
|
1531
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1532
|
+
this.updateButtons();
|
|
1533
|
+
}
|
|
1534
|
+
// Try ms
|
|
1535
|
+
else if (document.msExitFullscreen) {
|
|
1536
|
+
document.msExitFullscreen();
|
|
1537
|
+
this.isFullscreen = false;
|
|
1538
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1539
|
+
this.updateButtons();
|
|
1540
|
+
}
|
|
1541
|
+
// Fallback
|
|
1542
|
+
else {
|
|
1543
|
+
this.fallbackExitFullscreen();
|
|
1544
|
+
}
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
console.error('Zoomora: Error exiting fullscreen:', error);
|
|
1547
|
+
this.fallbackExitFullscreen();
|
|
1536
1548
|
}
|
|
1549
|
+
}
|
|
1537
1550
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
this.thumbs.classList.toggle('thumbs-hidden', !this.thumbnailsVisible);
|
|
1547
|
-
|
|
1548
|
-
if (!this.caption || !this.items[this.currentIndex]) return;
|
|
1551
|
+
/**
|
|
1552
|
+
* Fallback fullscreen using CSS
|
|
1553
|
+
*/
|
|
1554
|
+
fallbackFullscreen() {
|
|
1555
|
+
this.isFullscreen = true;
|
|
1556
|
+
this.zoomora.classList.add('fullscreen');
|
|
1557
|
+
this.updateButtons();
|
|
1558
|
+
}
|
|
1549
1559
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1560
|
+
/**
|
|
1561
|
+
* Exit CSS fullscreen
|
|
1562
|
+
*/
|
|
1563
|
+
fallbackExitFullscreen() {
|
|
1564
|
+
this.isFullscreen = false;
|
|
1565
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1566
|
+
this.updateButtons();
|
|
1567
|
+
}
|
|
1552
1568
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Toggle thumbnails visibility
|
|
1571
|
+
*/
|
|
1572
|
+
toggleThumbnails() {
|
|
1573
|
+
if (this.items.length === 1) return;
|
|
1561
1574
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
*/
|
|
1566
|
-
startDrag(e) {
|
|
1567
|
-
const media = this.content.querySelector('.zoomora-media');
|
|
1568
|
-
if (!media || !this.isZoomed) return;
|
|
1575
|
+
this.thumbnailsVisible = !this.thumbnailsVisible;
|
|
1576
|
+
if (this.thumbs) {
|
|
1577
|
+
this.thumbs.classList.toggle('thumbs-hidden', !this.thumbnailsVisible);
|
|
1569
1578
|
|
|
1570
|
-
this.
|
|
1571
|
-
media.classList.add('dragging');
|
|
1572
|
-
media.style.cursor = 'grabbing';
|
|
1579
|
+
if (!this.caption || !this.items[this.currentIndex]) return;
|
|
1573
1580
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1581
|
+
const item = this.items[this.currentIndex];
|
|
1582
|
+
const caption = item.dataset.caption || item.title || '';
|
|
1576
1583
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const matrix = new DOMMatrix(transform);
|
|
1581
|
-
this.currentX = matrix.m41;
|
|
1582
|
-
this.currentY = matrix.m42;
|
|
1584
|
+
if (caption && this.thumbs.classList.contains('thumbs-hidden')) {
|
|
1585
|
+
this.caption.style.bottom = '20px';
|
|
1586
|
+
this.caption.style.transition = 'bottom 0.3s ease, transform 0.3s ease';
|
|
1583
1587
|
} else {
|
|
1584
|
-
this.
|
|
1585
|
-
this.currentY = 0;
|
|
1588
|
+
this.caption.style.bottom = '120px';
|
|
1586
1589
|
}
|
|
1587
|
-
|
|
1588
|
-
e.preventDefault();
|
|
1589
1590
|
}
|
|
1591
|
+
}
|
|
1590
1592
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1593
|
+
/**
|
|
1594
|
+
* Start drag operation
|
|
1595
|
+
* @param {Event} e - Mouse or touch event
|
|
1596
|
+
*/
|
|
1597
|
+
startDrag(e) {
|
|
1598
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
1599
|
+
if (!media || !this.isZoomed) return;
|
|
1600
|
+
|
|
1601
|
+
this.isDragging = true;
|
|
1602
|
+
media.classList.add('dragging');
|
|
1603
|
+
media.style.cursor = 'grabbing';
|
|
1604
|
+
|
|
1605
|
+
this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
1606
|
+
this.startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
1607
|
+
|
|
1608
|
+
// Read current position from transform
|
|
1609
|
+
const transform = window.getComputedStyle(media).transform;
|
|
1610
|
+
if (transform && transform !== 'none') {
|
|
1611
|
+
const matrix = new DOMMatrix(transform);
|
|
1612
|
+
this.currentX = matrix.m41;
|
|
1613
|
+
this.currentY = matrix.m42;
|
|
1614
|
+
} else {
|
|
1615
|
+
this.currentX = 0;
|
|
1616
|
+
this.currentY = 0;
|
|
1617
|
+
}
|
|
1598
1618
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1619
|
+
e.preventDefault();
|
|
1620
|
+
}
|
|
1601
1621
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1622
|
+
/**
|
|
1623
|
+
* Handle drag movement
|
|
1624
|
+
* @param {Event} e - Mouse or touch event
|
|
1625
|
+
*/
|
|
1626
|
+
drag(e) {
|
|
1627
|
+
if (!this.isDragging) return;
|
|
1628
|
+
e.preventDefault();
|
|
1604
1629
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1630
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
1631
|
+
if (!media) return;
|
|
1607
1632
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1633
|
+
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
1634
|
+
const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
1610
1635
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
const mediaRect = media.getBoundingClientRect();
|
|
1636
|
+
const deltaX = clientX - this.startX;
|
|
1637
|
+
const deltaY = clientY - this.startY;
|
|
1614
1638
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
|
|
1639
|
+
let newX = this.currentX + deltaX;
|
|
1640
|
+
let newY = this.currentY + deltaY;
|
|
1618
1641
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
} else {
|
|
1623
|
-
newX = 0; // Center horizontally if no overflow
|
|
1624
|
-
}
|
|
1642
|
+
// Calculate boundaries for BOTH width AND height
|
|
1643
|
+
const container = this.content.getBoundingClientRect();
|
|
1644
|
+
const mediaRect = media.getBoundingClientRect();
|
|
1625
1645
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
newY = 0; // Center vertically if no overflow
|
|
1630
|
-
}
|
|
1646
|
+
// How much the scaled image overflows the container
|
|
1647
|
+
const overflowX = Math.max(0, (mediaRect.width - container.width) / 2);
|
|
1648
|
+
const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
|
|
1631
1649
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1650
|
+
// Constrain drag to overflow boundaries
|
|
1651
|
+
if (overflowX > 0) {
|
|
1652
|
+
newX = Math.max(-overflowX, Math.min(overflowX, newX));
|
|
1653
|
+
} else {
|
|
1654
|
+
newX = 0; // Center horizontally if no overflow
|
|
1655
|
+
}
|
|
1634
1656
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
this.currentY = newY;
|
|
1657
|
+
if (overflowY > 0) {
|
|
1658
|
+
newY = Math.max(-overflowY, Math.min(overflowY, newY));
|
|
1659
|
+
} else {
|
|
1660
|
+
newY = 0; // Center vertically if no overflow
|
|
1640
1661
|
}
|
|
1641
1662
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
*/
|
|
1645
|
-
endDrag() {
|
|
1646
|
-
if (!this.isDragging) return;
|
|
1663
|
+
// Apply transform
|
|
1664
|
+
media.style.transform = `translate(${newX}px, ${newY}px) scale(${this.currentScale})`;
|
|
1647
1665
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1666
|
+
// Update for next frame
|
|
1667
|
+
this.startX = clientX;
|
|
1668
|
+
this.startY = clientY;
|
|
1669
|
+
this.currentX = newX;
|
|
1670
|
+
this.currentY = newY;
|
|
1671
|
+
}
|
|
1650
1672
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1673
|
+
/**
|
|
1674
|
+
* End drag operation
|
|
1675
|
+
*/
|
|
1676
|
+
endDrag() {
|
|
1677
|
+
if (!this.isDragging) return;
|
|
1659
1678
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
* @param {number} index - Item index
|
|
1663
|
-
*/
|
|
1664
|
-
goTo(index) {
|
|
1665
|
-
if (index < 0 || index >= this.items.length) return;
|
|
1679
|
+
this.isDragging = false;
|
|
1680
|
+
this.lastDragTime = Date.now();
|
|
1666
1681
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
this.
|
|
1671
|
-
|
|
1672
|
-
|
|
1682
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
1683
|
+
if (media) {
|
|
1684
|
+
media.classList.remove('dragging');
|
|
1685
|
+
if (this.isZoomed) {
|
|
1686
|
+
media.style.cursor = 'grab';
|
|
1687
|
+
}
|
|
1673
1688
|
}
|
|
1689
|
+
}
|
|
1674
1690
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1691
|
+
/**
|
|
1692
|
+
* Go to specific item by index
|
|
1693
|
+
* @param {number} index - Item index
|
|
1694
|
+
*/
|
|
1695
|
+
goTo(index) {
|
|
1696
|
+
if (index < 0 || index >= this.items.length) return;
|
|
1697
|
+
|
|
1698
|
+
this.currentIndex = index;
|
|
1699
|
+
this.loadContent();
|
|
1700
|
+
this.updateCounter();
|
|
1701
|
+
this.updateThumbnails();
|
|
1702
|
+
this.updateNavigation();
|
|
1703
|
+
this.updateCaption();
|
|
1704
|
+
}
|
|
1682
1705
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1706
|
+
/**
|
|
1707
|
+
* Get current item index
|
|
1708
|
+
* @returns {number} - Current index
|
|
1709
|
+
*/
|
|
1710
|
+
getCurrentIndex() {
|
|
1711
|
+
return this.currentIndex;
|
|
1712
|
+
}
|
|
1690
1713
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1714
|
+
/**
|
|
1715
|
+
* Get total number of items
|
|
1716
|
+
* @returns {number} - Total items
|
|
1717
|
+
*/
|
|
1718
|
+
getTotalItems() {
|
|
1719
|
+
return this.items.length;
|
|
1720
|
+
}
|
|
1698
1721
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1722
|
+
/**
|
|
1723
|
+
* Check if lightbox is open
|
|
1724
|
+
* @returns {boolean} - True if open
|
|
1725
|
+
*/
|
|
1726
|
+
isOpen() {
|
|
1727
|
+
return this.zoomora && this.zoomora.classList.contains('active');
|
|
1728
|
+
}
|
|
1706
1729
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1730
|
+
/**
|
|
1731
|
+
* Update options
|
|
1732
|
+
* @param {Object} newOptions - New options to merge
|
|
1733
|
+
*/
|
|
1734
|
+
updateOptions(newOptions) {
|
|
1735
|
+
this.options = {...this.options, ...newOptions};
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Refresh the lightbox (re-scan for items)
|
|
1740
|
+
*/
|
|
1741
|
+
refresh() {
|
|
1742
|
+
if (this.isOpen()) {
|
|
1743
|
+
const currentItem = this.items[this.currentIndex];
|
|
1744
|
+
if (currentItem) {
|
|
1745
|
+
this.open(currentItem);
|
|
1716
1746
|
}
|
|
1717
1747
|
}
|
|
1748
|
+
}
|
|
1718
1749
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1750
|
+
/**
|
|
1751
|
+
* Destroy the lightbox instance
|
|
1752
|
+
*/
|
|
1753
|
+
destroy() {
|
|
1754
|
+
if (!this.isInitialized) return;
|
|
1724
1755
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1756
|
+
// Close if open
|
|
1757
|
+
if (this.isOpen()) {
|
|
1758
|
+
this.close();
|
|
1759
|
+
}
|
|
1729
1760
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1761
|
+
// Clean up auto-hide
|
|
1762
|
+
this.stopAutoHideTimer();
|
|
1732
1763
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
});
|
|
1764
|
+
// Remove all event listeners
|
|
1765
|
+
this.eventListeners.forEach((listeners, _key) => {
|
|
1766
|
+
listeners.forEach(({element, event, handler, options}) => {
|
|
1767
|
+
if (element && element.removeEventListener) {
|
|
1768
|
+
element.removeEventListener(event, handler, options);
|
|
1769
|
+
}
|
|
1740
1770
|
});
|
|
1741
|
-
|
|
1771
|
+
});
|
|
1772
|
+
this.eventListeners.clear();
|
|
1742
1773
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
// Reset properties
|
|
1749
|
-
this.isInitialized = false;
|
|
1750
|
-
this.items = [];
|
|
1751
|
-
this.currentIndex = 0;
|
|
1752
|
-
this.zoomora = null;
|
|
1753
|
-
this.content = null;
|
|
1754
|
-
this.counter = null;
|
|
1755
|
-
this.thumbs = null;
|
|
1756
|
-
this.loading = null;
|
|
1757
|
-
this.caption = null;
|
|
1774
|
+
// Remove DOM elements
|
|
1775
|
+
if (this.zoomora && this.zoomora.parentNode) {
|
|
1776
|
+
this.zoomora.parentNode.removeChild(this.zoomora);
|
|
1758
1777
|
}
|
|
1778
|
+
|
|
1779
|
+
// Reset properties
|
|
1780
|
+
this.isInitialized = false;
|
|
1781
|
+
this.items = [];
|
|
1782
|
+
this.currentIndex = 0;
|
|
1783
|
+
this.zoomora = null;
|
|
1784
|
+
this.content = null;
|
|
1785
|
+
this.counter = null;
|
|
1786
|
+
this.thumbs = null;
|
|
1787
|
+
this.loading = null;
|
|
1788
|
+
this.caption = null;
|
|
1759
1789
|
}
|
|
1790
|
+
}
|
|
1760
1791
|
|
|
1761
|
-
|
|
1762
|
-
return Zoomora;
|
|
1763
|
-
}));
|
|
1792
|
+
export default Zoomora;
|