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