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