zoomora 1.2.1 → 1.3.0

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