zoomora 1.3.0 → 1.4.0

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