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