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