zoomora 1.2.0 → 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,184 +1,249 @@
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.0
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">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
- <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
170
- <i class="zoomorai-thumbnails"></i>
171
- </button>
228
+ ${this.options.showThumbnails ? `
229
+ <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
230
+ <i class="zoomorai-thumbnails"></i>
231
+ </button>
232
+ ` : ""}
172
233
  ${autoHideButtonHTML}
173
- <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
174
- <i class="zoomorai-zoom"></i>
175
- </button>
176
- <button class="zoomora-btn" id="zoomoraFullscreenBtn" title="Fullscreen">
177
- <i class="zoomorai-expand"></i>
178
- </button>
179
- <button class="zoomora-btn" id="zoomoraCollapseBtn" title="Exit Fullscreen" style="display: none;">
180
- <i class="zoomorai-collapse"></i>
181
- </button>
234
+ ${this.options.showZoom ? `
235
+ <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
236
+ <i class="zoomorai-zoom"></i>
237
+ </button>
238
+ ` : ""}
239
+ ${this.options.showFullscreen ? `
240
+ <button class="zoomora-btn" id="zoomoraFullscreenBtn" title="Fullscreen">
241
+ <i class="zoomorai-expand"></i>
242
+ </button>
243
+ <button class="zoomora-btn" id="zoomoraCollapseBtn" title="Exit Fullscreen" style="display: none;">
244
+ <i class="zoomorai-collapse"></i>
245
+ </button>
246
+ ` : ""}
182
247
  <button class="zoomora-btn" id="zoomoraCloseBtn" title="Close">
183
248
  <i class="zoomorai-close"></i>
184
249
  </button>
@@ -197,1117 +262,1096 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
197
262
  <i class="zoomorai-arrow-right"></i>
198
263
  </button>
199
264
 
200
- <div class="zoomora-caption" id="zoomoraCaption"></div>
201
- <div class="zoomora-thumbnails" id="zoomoraThumbs"></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>
202
267
  </div>
203
268
  </div>
204
269
  `;
205
- document.body.insertAdjacentHTML("beforeend", zoomoraHTML);
206
- this.zoomora = document.getElementById("zoomoraLightbox");
207
- this.header = document.getElementById("zoomoraHeader");
208
- this.content = document.getElementById("zoomoraContent");
209
- this.slidesContainer = document.getElementById("zoomoraSlidesContainer");
210
- this.counter = document.getElementById("zoomoraCounter");
211
- this.thumbs = document.getElementById("zoomoraThumbs");
212
- this.loading = document.getElementById("zoomoraLoading");
213
- this.caption = document.getElementById("zoomoraCaption");
214
- this.prevBtn = document.getElementById("zoomoraPrevBtn");
215
- this.nextBtn = document.getElementById("zoomoraNextBtn");
216
- if (!this.zoomora || !this.content || !this.counter || !this.thumbs || !this.loading || !this.caption) {
217
- throw new Error("Failed to create required DOM elements");
218
- }
219
- }
220
- /**
221
- * Bind event listeners
222
- */
223
- bindEvents() {
224
- this.addEventListener(document, "click", this.boundMethods.handleDocumentClick);
225
- this.addEventListener(document.getElementById("zoomoraCloseBtn"), "click", () => this.close());
226
- this.addEventListener(document.getElementById("zoomoraPrevBtn"), "click", () => this.prev());
227
- this.addEventListener(document.getElementById("zoomoraNextBtn"), "click", () => this.next());
228
- this.addEventListener(document.getElementById("zoomoraZoomBtn"), "click", () => this.toggleZoom());
229
- this.addEventListener(document.getElementById("zoomoraFullscreenBtn"), "click", () => this.toggleFullscreen());
230
- this.addEventListener(document.getElementById("zoomoraCollapseBtn"), "click", () => this.exitFullscreen());
231
- this.addEventListener(document.getElementById("zoomoraThumbnailBtn"), "click", () => this.toggleThumbnails());
232
- this.addEventListener(document, "fullscreenchange", this.boundMethods.handleFullscreenChange);
233
- this.addEventListener(document, "webkitfullscreenchange", this.boundMethods.handleFullscreenChange);
234
- this.addEventListener(document, "mozfullscreenchange", this.boundMethods.handleFullscreenChange);
235
- this.addEventListener(document, "MSFullscreenChange", this.boundMethods.handleFullscreenChange);
236
- this.addEventListener(document, "keydown", this.boundMethods.handleKeydown);
237
- this.addEventListener(this.zoomora, "click", this.boundMethods.handleZoomoraClick);
238
- this.addEventListener(this.content, "click", this.boundMethods.handleContentClick);
239
- this.addEventListener(this.content, "mousedown", this.boundMethods.handleMouseDown);
240
- this.addEventListener(this.content, "mousemove", this.boundMethods.handleMouseMove);
241
- this.addEventListener(this.content, "mouseup", this.boundMethods.handleMouseUp);
242
- this.addEventListener(this.content, "mouseleave", this.boundMethods.handleMouseUp);
243
- this.addEventListener(this.content, "touchstart", this.boundMethods.handleTouchStart, { passive: false });
244
- this.addEventListener(this.content, "touchmove", this.boundMethods.handleTouchMove, { passive: false });
245
- this.addEventListener(this.content, "touchend", this.boundMethods.handleTouchEnd);
246
- if (this.options.showAutoHideToggle) {
247
- this.addEventListener(document.getElementById("zoomoraAutoHideBtn"), "click", () => this.toggleAutoHide());
248
- this.addEventListener(this.zoomora, "mousemove", () => this.handleUserActivity());
249
- this.addEventListener(this.zoomora, "touchstart", () => this.handleUserActivity());
250
- this.addEventListener(this.zoomora, "keydown", () => this.handleUserActivity());
251
- }
252
- }
253
- /**
254
- * Add event listener with cleanup tracking
255
- * @param {Element} element - DOM element
256
- * @param {string} event - Event type
257
- * @param {Function} handler - Event handler
258
- * @param {Object} options - Event options
259
- */
260
- addEventListener(element, event, handler, options = {}) {
261
- if (!element) return;
262
- element.addEventListener(event, handler, options);
263
- const key = `${element.constructor.name}_${event}`;
264
- if (!this.eventListeners.has(key)) {
265
- this.eventListeners.set(key, []);
266
- }
267
- this.eventListeners.get(key).push({ element, event, handler, options });
268
- }
269
- /**
270
- * Handle document click events
271
- * @param {Event} e - Click event
272
- */
273
- handleDocumentClick(e) {
274
- const target = e.target.closest(this.options.selector);
275
- if (target) {
276
- e.preventDefault();
277
- this.open(target);
278
- }
279
- }
280
- /**
281
- * Handle keyboard events
282
- * @param {KeyboardEvent} e - Keyboard event
283
- */
284
- handleKeydown(e) {
285
- if (!this.zoomora.classList.contains("active")) return;
286
- switch (e.key) {
287
- case "Escape":
288
- if (this.isFullscreen) {
289
- this.exitFullscreen();
290
- } else {
291
- this.close();
292
- }
293
- break;
294
- case "ArrowLeft":
295
- this.prev();
296
- break;
297
- case "ArrowRight":
298
- this.next();
299
- break;
300
- case "z":
301
- case "Z":
302
- this.toggleZoom();
303
- break;
304
- case "f":
305
- case "F":
306
- this.toggleFullscreen();
307
- break;
308
- case "t":
309
- case "T":
310
- this.toggleThumbnails();
311
- break;
312
- }
313
- }
314
- /**
315
- * Handle fullscreen change events
316
- */
317
- handleFullscreenChange() {
318
- const isInFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
319
- if (!isInFullscreen && this.isFullscreen) {
320
- this.isFullscreen = false;
321
- this.zoomora.classList.remove("fullscreen");
322
- this.updateButtons();
323
- } else if (isInFullscreen && !this.isFullscreen) {
324
- this.isFullscreen = true;
325
- this.zoomora.classList.add("fullscreen");
326
- this.updateButtons();
327
- }
328
- }
329
- /**
330
- * Handle zoomora container clicks
331
- * @param {Event} e - Click event
332
- */
333
- handleZoomoraClick(e) {
334
- if (e.target === this.zoomora && this.options.closeOnBackgroundClick) {
335
- this.close();
336
- }
337
- }
338
- /**
339
- * Handle content clicks
340
- * @param {Event} e - Click event
341
- */
342
- handleContentClick(e) {
343
- if (Date.now() - this.lastDragTime < 200) {
344
- return;
345
- }
346
- if (this.header && this.header.contains(e.target)) {
347
- return;
348
- }
349
- if (e.target.closest("button")) {
350
- return;
351
- }
352
- const mediaElement = e.target.closest(".zoomora-media, .zoomora-video, .zoomora-iframe");
353
- if (mediaElement) {
354
- if (mediaElement.classList.contains("zoomora-media") && !mediaElement.classList.contains("no-zoom")) {
355
- this.toggleZoom();
356
- }
357
- return;
358
- }
359
- const videoContainer = e.target.closest(".zoomora-video-container");
360
- if (videoContainer) {
361
- if (e.target.closest(".zoomora-play-button")) {
362
- return;
363
- }
364
- if (this.options.closeOnBackgroundClick) {
365
- this.close();
366
- }
367
- return;
368
- }
369
- const clickedElement = e.target;
370
- 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";
371
- if (isBackgroundElement && this.options.closeOnBackgroundClick) {
372
- this.close();
373
- return;
374
- }
375
- if (this.options.closeOnBackgroundClick) {
376
- this.close();
377
- }
378
- }
379
- /**
380
- * Handle mouse down events
381
- * @param {MouseEvent} e - Mouse event
382
- */
383
- handleMouseDown(e) {
384
- this.startDrag(e);
385
- }
386
- /**
387
- * Handle mouse move events
388
- * @param {MouseEvent} e - Mouse event
389
- */
390
- handleMouseMove(e) {
391
- this.drag(e);
392
- }
393
- /**
394
- * Handle mouse up events
395
- */
396
- handleMouseUp() {
397
- this.endDrag();
398
- }
399
- /**
400
- * Handle touch start events
401
- * @param {TouchEvent} e - Touch event
402
- */
403
- handleTouchStart(e) {
404
- this.startDrag(e);
405
- }
406
- /**
407
- * Handle touch move events
408
- * @param {TouchEvent} e - Touch event
409
- */
410
- handleTouchMove(e) {
411
- this.drag(e);
412
- }
413
- /**
414
- * Handle touch end events
415
- */
416
- handleTouchEnd() {
417
- this.endDrag();
418
- }
419
- /**
420
- * Get image source from element
421
- * @param {Element} item - DOM element
422
- * @returns {string} - Image source URL
423
- */
424
- getImageSource(item) {
425
- const src = item.dataset.src || item.src || item.getAttribute("src");
426
- const type = item.dataset.type || "image";
427
- if (type === "video" && src) {
428
- return this.convertToEmbedUrl(src);
429
- }
430
- return src;
431
- }
432
- /**
433
- * Convert YouTube URLs to embed format
434
- * @param url
435
- * @returns {*|string}
436
- */
437
- convertToEmbedUrl(url) {
438
- if (url.includes("youtube.com/embed/")) {
439
- return url;
440
- }
441
- let videoId = null;
442
- let startTime = null;
443
- if (url.includes("youtube.com/watch")) {
444
- const urlParams = new URLSearchParams(url.split("?")[1]);
445
- videoId = urlParams.get("v");
446
- const t = urlParams.get("t");
447
- if (t) {
448
- startTime = t.replace("s", "");
449
- }
450
- } else if (url.includes("youtu.be/")) {
451
- const parts = url.split("/");
452
- const videoAndParams = parts[parts.length - 1];
453
- if (videoAndParams.includes("?")) {
454
- videoId = videoAndParams.split("?")[0];
455
- const urlParams = new URLSearchParams(videoAndParams.split("?")[1]);
456
- const t = urlParams.get("t");
457
- if (t) startTime = t.replace("s", "");
458
- } else {
459
- videoId = videoAndParams;
460
- }
461
- }
462
- if (videoId) {
463
- let embedUrl = `https://www.youtube.com/embed/${videoId}`;
464
- const params = new URLSearchParams();
465
- if (startTime) {
466
- params.append("start", startTime);
467
- }
468
- params.append("rel", "0");
469
- params.append("modestbranding", "1");
470
- if (params.toString()) {
471
- embedUrl += "?" + params.toString();
472
- }
473
- return embedUrl;
474
- }
475
- return url;
476
- }
477
- /**
478
- * Get thumbnail source from element
479
- * @param {Element} item - DOM element
480
- * @returns {string} - Thumbnail source URL
481
- */
482
- getThumbnailSource(item) {
483
- const type = item.dataset.type || "image";
484
- if (type === "video") {
485
- return item.src || item.getAttribute("src");
486
- }
487
- return item.dataset.src || item.src || item.getAttribute("src");
488
- }
489
- /**
490
- * Get group identifier from element
491
- * @param {Element} element - DOM element
492
- * @returns {string} - Group identifier
493
- */
494
- getGroup(element) {
495
- if (this.options.useHref) {
496
- return element.dataset.lightbox || element.dataset.gallery || "default";
497
- }
498
- return element.dataset.zoomora || "default";
499
- }
500
- /**
501
- * Open lightbox with specified element
502
- * @param {Element} element - DOM element to open
503
- */
504
- open(element) {
505
- if (!element) return;
506
- const group = this.getGroup(element);
507
- let groupSelector;
508
- if (this.options.useHref) {
509
- if (element.dataset.lightbox) {
510
- groupSelector = `[data-lightbox="${group}"]`;
511
- } else if (element.dataset.gallery) {
512
- groupSelector = `[data-gallery="${group}"]`;
513
- } else {
514
- groupSelector = this.options.selector;
515
- }
516
- } else {
517
- groupSelector = `[data-zoomora="${group}"]`;
518
- }
519
- this.items = Array.from(document.querySelectorAll(groupSelector));
520
- this.currentIndex = Math.max(0, this.items.indexOf(element));
521
- this.isZoomed = false;
522
- this.isFullscreen = false;
523
- this.thumbnailsVisible = true;
524
- this.currentX = 0;
525
- this.currentY = 0;
526
- this.currentScale = 1;
527
- this.zoomora.classList.add("active");
528
- document.body.style.overflow = "hidden";
529
- this.loadContent();
530
- this.updateThumbnails();
531
- this.updateCounter();
532
- this.updateNavigation();
533
- this.updateCaption();
534
- this.updateButtons();
535
- if (this.autoHideEnabled) {
536
- this.startAutoHideTimer();
537
- } else {
538
- this.showControls();
539
- }
540
- if (typeof this.options.onOpen === "function") {
541
- this.options.onOpen(element, this.currentIndex);
542
- }
543
- }
544
- /**
545
- * Close lightbox
546
- */
547
- close() {
548
- if (this.isFullscreen) {
549
- this.exitFullscreen();
550
- }
551
- this.zoomora.classList.remove("active");
552
- document.body.style.overflow = "";
553
- this.isZoomed = false;
554
- this.isFullscreen = false;
555
- this.currentX = 0;
556
- this.currentY = 0;
557
- this.currentScale = 1;
558
- const media = this.content.querySelector(".zoomora-media");
559
- if (media) {
560
- media.classList.remove("zoomed");
561
- media.style.transform = "";
562
- media.style.cursor = "";
563
- }
564
- this.zoomora.classList.remove("fullscreen");
565
- this.updateButtons();
566
- this.stopAutoHideTimer();
567
- this.showControls();
568
- if (typeof this.options.onClose === "function") {
569
- this.options.onClose(this.currentIndex);
570
- }
571
- }
572
- /**
573
- * Navigate to previous item
574
- */
575
- prev() {
576
- if (this.currentIndex > 0) {
577
- this.currentIndex--;
578
- this.loadContent();
579
- this.updateCounter();
580
- this.updateThumbnails();
581
- this.updateNavigation();
582
- this.updateCaption();
583
- if (typeof this.options.onPrev === "function") {
584
- this.options.onPrev(this.currentIndex);
585
- }
586
- }
587
- }
588
- /**
589
- * Navigate to next item
590
- */
591
- next() {
592
- if (this.currentIndex < this.items.length - 1) {
593
- this.currentIndex++;
594
- this.loadContent();
595
- this.updateCounter();
596
- this.updateThumbnails();
597
- this.updateNavigation();
598
- this.updateCaption();
599
- if (typeof this.options.onNext === "function") {
600
- this.options.onNext(this.currentIndex);
601
- }
602
- }
603
- }
604
- /**
605
- * Load content for current item
606
- */
607
- loadContent() {
608
- if (!this.items[this.currentIndex]) return;
609
- const item = this.items[this.currentIndex];
610
- const src = this.getImageSource(item);
611
- const type = item.dataset.type || "image";
612
- if (!src) {
613
- console.error("Zoomora: No source found for item");
614
- return;
615
- }
616
- this.isZoomed = false;
617
- this.currentX = 0;
618
- this.currentY = 0;
619
- this.currentScale = 1;
620
- this.loading.style.display = "block";
621
- if (this.slidesContainer) {
622
- this.slidesContainer.innerHTML = "";
623
- }
624
- setTimeout(() => {
625
- this.renderContent(src, type, item);
626
- }, 100);
627
- }
628
- /**
629
- * Render content based on type
630
- * @param {string} src - Source URL
631
- * @param {string} type - Content type
632
- * @param {Element} item - Original item element
633
- */
634
- renderContent(src, type, item) {
635
- const slideContainer = document.createElement("div");
636
- slideContainer.className = "zoomora-slide-container";
637
- const slide = document.createElement("div");
638
- slide.className = `zoomora-slide ${this.options.transition === "slide" ? "slide-effect" : "fade-effect"}`;
639
- slide.style.opacity = "0";
640
- slide.style.transform = this.options.transition === "slide" ? "translateX(100%)" : "scale(0.8)";
641
- let mediaElement;
642
- try {
643
- switch (type) {
644
- case "video":
645
- if (src.endsWith(".mp4") || src.endsWith(".webm") || src.endsWith(".ogg")) {
646
- const videoContainer = document.createElement("div");
647
- videoContainer.className = "zoomora-video-container";
648
- mediaElement = document.createElement("video");
649
- mediaElement.src = src;
650
- mediaElement.className = "zoomora-video";
651
- mediaElement.setAttribute("preload", "metadata");
652
- const posterSrc = item.src || item.getAttribute("src");
653
- if (posterSrc) {
654
- mediaElement.setAttribute("poster", posterSrc);
655
- }
656
- const playButton = document.createElement("div");
657
- playButton.className = "zoomora-play-button";
658
- playButton.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
659
- playButton.addEventListener("click", () => {
660
- mediaElement.play();
661
- playButton.style.display = "none";
662
- mediaElement.setAttribute("controls", "");
663
- });
664
- mediaElement.addEventListener("ended", () => {
665
- playButton.style.display = "flex";
666
- });
667
- videoContainer.appendChild(mediaElement);
668
- videoContainer.appendChild(playButton);
669
- slideContainer.appendChild(videoContainer);
670
- } else {
671
- mediaElement = document.createElement("iframe");
672
- mediaElement.src = src;
673
- mediaElement.className = "zoomora-video";
674
- mediaElement.setAttribute("allowfullscreen", "");
675
- mediaElement.setAttribute("frameborder", "0");
676
- slideContainer.appendChild(mediaElement);
677
- }
678
- break;
679
- case "iframe":
680
- mediaElement = document.createElement("iframe");
681
- mediaElement.src = src;
682
- mediaElement.className = "zoomora-iframe";
683
- mediaElement.setAttribute("frameborder", "0");
684
- slideContainer.appendChild(mediaElement);
685
- break;
686
- default:
687
- mediaElement = document.createElement("img");
688
- mediaElement.src = src;
689
- mediaElement.className = "zoomora-media";
690
- mediaElement.alt = item.alt || item.title || "";
691
- mediaElement.onload = () => {
692
- this.loading.style.display = "none";
693
- this.checkZoomability(mediaElement);
694
- this.animateSlideIn(slide);
695
- };
696
- mediaElement.onerror = () => {
697
- this.loading.style.display = "none";
698
- console.error("Zoomora: Failed to load image:", src);
699
- this.animateSlideIn(slide);
700
- };
701
- slideContainer.appendChild(mediaElement);
702
- }
703
- slide.appendChild(slideContainer);
704
- this.slidesContainer.innerHTML = "";
705
- this.slidesContainer.appendChild(slide);
706
- if (type !== "image") {
707
- this.loading.style.display = "none";
708
- this.animateSlideIn(slide);
709
- }
710
- } catch (error) {
711
- console.error("Zoomora: Error rendering content:", error);
712
- this.loading.style.display = "none";
713
- }
714
- }
715
- // Animation helper
716
- animateSlideIn(slide) {
717
- slide.offsetHeight;
718
- slide.classList.add("active");
719
- if (this.options.transition === "slide") {
720
- slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
721
- slide.style.transform = "translateX(0)";
722
- slide.style.opacity = "1";
723
- } else {
724
- slide.style.transition = `transform ${this.options.animationDuration}ms ease, opacity ${this.options.animationDuration}ms ease`;
725
- slide.style.transform = "scale(1)";
726
- slide.style.opacity = "1";
727
- }
728
- }
729
- // Toggle auto-hide controls
730
- toggleAutoHide() {
731
- this.autoHideEnabled = !this.autoHideEnabled;
732
- const autoHideBtn = document.getElementById("zoomoraAutoHideBtn");
733
- if (this.autoHideEnabled) {
734
- autoHideBtn.classList.add("active");
735
- autoHideBtn.title = "Disable Auto-Hide Controls";
736
- this.startAutoHideTimer();
737
- } else {
738
- autoHideBtn.classList.remove("active");
739
- autoHideBtn.title = "Enable Auto-Hide Controls";
740
- this.stopAutoHideTimer();
741
- this.showControls();
742
- }
743
- }
744
- // Handle user activity
745
- handleUserActivity() {
746
- if (!this.autoHideEnabled) return;
747
- this.showControls();
748
- this.startAutoHideTimer();
749
- }
750
- // Start auto-hide timer
751
- startAutoHideTimer() {
752
- this.stopAutoHideTimer();
753
- this.autoHideTimeout = setTimeout(() => {
754
- this.hideControls();
755
- }, this.options.autoHideDelay);
756
- }
757
- // Stop auto-hide timer
758
- stopAutoHideTimer() {
759
- if (this.autoHideTimeout) {
760
- clearTimeout(this.autoHideTimeout);
761
- this.autoHideTimeout = null;
762
- }
763
- }
764
- // Show controls
765
- showControls() {
766
- if (this.isControlsVisible) return;
767
- this.isControlsVisible = true;
768
- this.zoomora.classList.remove("controls-hidden");
769
- if (this.header) {
770
- this.header.style.transform = "translateY(0)";
771
- this.header.style.opacity = "1";
772
- }
773
- if (this.caption) {
774
- this.caption.style.transform = "translateX(-50%) translateY(0)";
775
- this.caption.style.opacity = "1";
776
- }
777
- if (this.thumbs) {
778
- this.thumbs.style.transform = "translateX(-50%) translateY(0)";
779
- this.thumbs.style.opacity = "1";
780
- }
781
- if (this.prevBtn) {
782
- this.prevBtn.style.transform = "translateX(0)";
783
- this.prevBtn.style.opacity = "1";
784
- }
785
- if (this.nextBtn) {
786
- this.nextBtn.style.transform = "translateX(0)";
787
- this.nextBtn.style.opacity = "1";
788
- }
789
- }
790
- // Hide controls
791
- hideControls() {
792
- if (!this.isControlsVisible) return;
793
- this.isControlsVisible = false;
794
- this.zoomora.classList.add("controls-hidden");
795
- if (this.header) {
796
- this.header.style.transform = "translateY(-100%)";
797
- this.header.style.opacity = "0";
798
- }
799
- if (this.caption) {
800
- this.caption.style.transform = "translateX(-50%) translateY(100%)";
801
- this.caption.style.opacity = "0";
802
- }
803
- if (this.thumbs) {
804
- this.thumbs.style.transform = "translateX(-50%) translateY(100%)";
805
- this.thumbs.style.opacity = "0";
806
- }
807
- if (this.prevBtn) {
808
- this.prevBtn.style.transform = "translateX(-100%)";
809
- this.prevBtn.style.opacity = "0";
810
- }
811
- if (this.nextBtn) {
812
- this.nextBtn.style.transform = "translateX(100%)";
813
- this.nextBtn.style.opacity = "0";
814
- }
815
- }
816
- /**
817
- * Check if image can be zoomed
818
- * @param {HTMLImageElement} img - Image element
819
- */
820
- checkZoomability(img) {
821
- if (!img || !img.naturalWidth || !img.naturalHeight) return;
822
- const imgRect = img.getBoundingClientRect();
823
- const renderedWidth = imgRect.width;
824
- const renderedHeight = imgRect.height;
825
- const naturalWidth = img.naturalWidth;
826
- const naturalHeight = img.naturalHeight;
827
- const isScaledDown = naturalWidth > renderedWidth || naturalHeight > renderedHeight;
828
- const zoomRatioX = naturalWidth / renderedWidth;
829
- const zoomRatioY = naturalHeight / renderedHeight;
830
- const maxZoomRatio = Math.max(zoomRatioX, zoomRatioY);
831
- img.dataset.zoomRatio = maxZoomRatio;
832
- img.dataset.naturalWidth = naturalWidth;
833
- img.dataset.naturalHeight = naturalHeight;
834
- img.dataset.renderedWidth = renderedWidth;
835
- img.dataset.renderedHeight = renderedHeight;
836
- if (isScaledDown && maxZoomRatio > 1.1) {
837
- img.classList.remove("no-zoom");
838
- } else {
839
- img.classList.add("no-zoom");
840
- }
841
- }
842
- /**
843
- * Calculate zoom levels for an image
844
- * @param {HTMLImageElement} img - Image element
845
- * @returns {Object} Zoom configuration
846
- */
847
- calculateZoomLevels(img) {
848
- if (!img || !img.naturalWidth || !img.naturalHeight) {
849
- return { canZoom: false, levels: [], currentLevel: 0 };
850
- }
851
- const container = this.content.getBoundingClientRect();
852
- const containerWidth = container.width;
853
- const containerHeight = container.height;
854
- const naturalWidth = img.naturalWidth;
855
- const naturalHeight = img.naturalHeight;
856
- const scaleToFitWidth = containerWidth / naturalWidth;
857
- const scaleToFitHeight = containerHeight / naturalHeight;
858
- const fitScale = Math.min(scaleToFitWidth, scaleToFitHeight);
859
- if (fitScale >= 0.95) {
860
- return { canZoom: false, levels: [], currentLevel: 0 };
861
- }
862
- const levels = [1];
863
- if (fitScale * 1.5 <= 1) levels.push(1.5);
864
- if (fitScale * 2 <= 1) levels.push(2);
865
- if (fitScale * 3 <= 1) levels.push(3);
866
- const actualSizeScale = 1 / fitScale;
867
- if (actualSizeScale > levels[levels.length - 1]) {
868
- levels.push(actualSizeScale);
869
- }
870
- return {
871
- canZoom: levels.length > 1,
872
- levels,
873
- currentLevel: 0,
874
- baseScale: fitScale,
875
- naturalWidth,
876
- naturalHeight
877
- };
878
- }
879
- /**
880
- * Update counter display
881
- */
882
- updateCounter() {
883
- if (this.options.showCounter && this.counter) {
884
- this.counter.textContent = `${this.currentIndex + 1} / ${this.items.length}`;
885
- }
886
- }
887
- /**
888
- * Update caption display
889
- */
890
- updateCaption() {
891
- if (!this.caption || !this.items[this.currentIndex]) return;
892
- const item = this.items[this.currentIndex];
893
- const caption = item.dataset.caption || item.title || "";
894
- if (caption) {
895
- this.caption.textContent = caption;
896
- this.caption.style.transform = "translateX(-50%) translateY(0)";
897
- this.caption.style.opacity = "1";
898
- this.caption.style.pointerEvents = "auto";
899
- this.caption.style.transition = "opacity 0.3s ease, transform 0.3s ease";
900
- } else {
901
- this.caption.style.transform = "translateX(-50%) translateY(100%)";
902
- this.caption.style.opacity = "0";
903
- this.caption.style.pointerEvents = "none";
904
- this.caption.style.transition = "opacity 0.3s ease, transform 0.3s ease";
905
- }
906
- }
907
- /**
908
- * Update thumbnails display
909
- */
910
- updateThumbnails() {
911
- if (!this.options.showThumbnails || !this.thumbs) return;
912
- if (this.items.length === 1) {
913
- this.caption.classList.add("single-item-thumbnail");
914
- this.thumbs.style.display = "none";
915
- return;
916
- }
917
- this.caption.classList.remove("single-item-thumbnail");
918
- this.thumbs.style.display = "flex";
919
- this.thumbs.innerHTML = "";
920
- this.items.forEach((item, index) => {
921
- const thumb = document.createElement("div");
922
- thumb.className = "zoomora-thumb";
923
- if (index === this.currentIndex) thumb.classList.add("active");
924
- const img = document.createElement("img");
925
- const thumbSrc = this.getThumbnailSource(item);
926
- if (thumbSrc) {
927
- img.src = thumbSrc;
928
- img.alt = item.alt || item.title || "";
929
- img.onerror = () => {
930
- thumb.style.display = "none";
931
- };
932
- }
933
- thumb.appendChild(img);
934
- thumb.addEventListener("click", () => {
935
- this.currentIndex = index;
936
- this.loadContent();
937
- this.updateCounter();
938
- this.updateThumbnails();
939
- this.updateNavigation();
940
- this.updateCaption();
941
- });
942
- this.thumbs.appendChild(thumb);
943
- });
944
- }
945
- /**
946
- * Update navigation buttons
947
- */
948
- updateNavigation() {
949
- const prevBtn = document.getElementById("zoomoraPrevBtn");
950
- const nextBtn = document.getElementById("zoomoraNextBtn");
951
- if (prevBtn) {
952
- prevBtn.style.display = this.currentIndex > 0 ? "flex" : "none";
953
- }
954
- if (nextBtn) {
955
- nextBtn.style.display = this.currentIndex < this.items.length - 1 ? "flex" : "none";
956
- }
957
- }
958
- /**
959
- * Update control buttons
960
- */
961
- updateButtons() {
962
- const fullscreenBtn = document.getElementById("zoomoraFullscreenBtn");
963
- const collapseBtn = document.getElementById("zoomoraCollapseBtn");
964
- if (fullscreenBtn && collapseBtn) {
965
- if (this.isFullscreen) {
966
- fullscreenBtn.style.display = "none";
967
- collapseBtn.style.display = "flex";
968
- } else {
969
- fullscreenBtn.style.display = "flex";
970
- collapseBtn.style.display = "none";
971
- }
972
- }
973
- }
974
- /**
975
- * Toggle zoom state
976
- */
977
- toggleZoom() {
978
- const media = this.content.querySelector(".zoomora-media");
979
- if (!media || media.tagName !== "IMG") return;
980
- const zoomConfig = this.calculateZoomLevels(media);
981
- if (!zoomConfig.canZoom) {
982
- return;
983
- }
984
- if (!media.dataset.zoomLevel) {
985
- media.dataset.zoomLevel = "0";
986
- }
987
- let currentLevel = parseInt(media.dataset.zoomLevel) || 0;
988
- currentLevel++;
989
- if (currentLevel >= zoomConfig.levels.length) {
990
- currentLevel = 0;
991
- }
992
- const targetScale = zoomConfig.levels[currentLevel];
993
- media.dataset.zoomLevel = currentLevel.toString();
994
- if (currentLevel === 0) {
995
- this.isZoomed = false;
996
- this.isDragging = false;
997
- media.classList.remove("dragging");
998
- media.style.transform = "";
999
- media.style.cursor = "pointer";
1000
- media.classList.remove("zoomed");
1001
- this.currentX = 0;
1002
- this.currentY = 0;
1003
- this.currentScale = 1;
1004
- } else {
1005
- this.isZoomed = true;
1006
- this.currentScale = targetScale;
1007
- this.currentX = 0;
1008
- this.currentY = 0;
1009
- media.style.transform = `translate(0px, 0px) scale(${targetScale})`;
1010
- media.style.cursor = "grab";
1011
- media.classList.add("zoomed");
1012
- }
1013
- }
1014
- /**
1015
- * Update image transform based on current state
1016
- * @param {HTMLElement} media - Media element
1017
- */
1018
- updateImageTransform(media) {
1019
- if (!media) return;
1020
- if (this.currentScale === 1) {
1021
- media.style.transform = "";
1022
- } else {
1023
- const translateX = this.currentX / this.currentScale;
1024
- const translateY = this.currentY / this.currentScale;
1025
- media.style.transform = `scale(${this.currentScale}) translate(${translateX}px, ${translateY}px)`;
1026
- }
1027
- }
1028
- /**
1029
- * Toggle fullscreen mode
1030
- */
1031
- toggleFullscreen() {
1032
- if (!this.isFullscreen) {
1033
- this.enterFullscreen();
1034
- } else {
1035
- this.exitFullscreen();
1036
- }
1037
- }
1038
- /**
1039
- * Enter fullscreen mode
1040
- */
1041
- enterFullscreen() {
1042
- const element = this.zoomora;
1043
- try {
1044
- if (element.requestFullscreen) {
1045
- element.requestFullscreen().then(() => {
1046
- this.isFullscreen = true;
1047
- this.zoomora.classList.add("fullscreen");
1048
- this.updateButtons();
1049
- }).catch((err) => {
1050
- console.warn("Zoomora: Fullscreen request failed:", err.message);
1051
- this.fallbackFullscreen();
1052
- });
1053
- } else if (element.webkitRequestFullscreen) {
1054
- element.webkitRequestFullscreen();
1055
- this.isFullscreen = true;
1056
- this.zoomora.classList.add("fullscreen");
1057
- this.updateButtons();
1058
- } else if (element.mozRequestFullScreen) {
1059
- element.mozRequestFullScreen();
1060
- this.isFullscreen = true;
1061
- this.zoomora.classList.add("fullscreen");
1062
- this.updateButtons();
1063
- } else if (element.msRequestFullscreen) {
1064
- element.msRequestFullscreen();
1065
- this.isFullscreen = true;
1066
- this.zoomora.classList.add("fullscreen");
1067
- this.updateButtons();
1068
- } else {
1069
- console.warn("Zoomora: Fullscreen API not supported, using CSS fallback");
1070
- this.fallbackFullscreen();
1071
- }
1072
- } catch (error) {
1073
- console.error("Zoomora: Error entering fullscreen:", error);
1074
- this.fallbackFullscreen();
1075
- }
1076
- }
1077
- /**
1078
- * Exit fullscreen mode
1079
- */
1080
- exitFullscreen() {
1081
- const isInFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
1082
- if (!isInFullscreen) {
1083
- this.fallbackExitFullscreen();
1084
- return;
1085
- }
1086
- try {
1087
- if (document.exitFullscreen) {
1088
- document.exitFullscreen().then(() => {
1089
- this.isFullscreen = false;
1090
- this.zoomora.classList.remove("fullscreen");
1091
- this.updateButtons();
1092
- }).catch((err) => {
1093
- console.warn("Zoomora: Exit fullscreen failed:", err.message);
1094
- this.fallbackExitFullscreen();
1095
- });
1096
- } else if (document.webkitExitFullscreen) {
1097
- document.webkitExitFullscreen();
1098
- this.isFullscreen = false;
1099
- this.zoomora.classList.remove("fullscreen");
1100
- this.updateButtons();
1101
- } else if (document.mozCancelFullScreen) {
1102
- document.mozCancelFullScreen();
1103
- this.isFullscreen = false;
1104
- this.zoomora.classList.remove("fullscreen");
1105
- this.updateButtons();
1106
- } else if (document.msExitFullscreen) {
1107
- document.msExitFullscreen();
1108
- this.isFullscreen = false;
1109
- this.zoomora.classList.remove("fullscreen");
1110
- this.updateButtons();
1111
- } else {
1112
- this.fallbackExitFullscreen();
1113
- }
1114
- } catch (error) {
1115
- console.error("Zoomora: Error exiting fullscreen:", error);
1116
- this.fallbackExitFullscreen();
1117
- }
1118
- }
1119
- /**
1120
- * Fallback fullscreen using CSS
1121
- */
1122
- fallbackFullscreen() {
1123
- this.isFullscreen = true;
1124
- this.zoomora.classList.add("fullscreen");
1125
- this.updateButtons();
1126
- }
1127
- /**
1128
- * Exit CSS fullscreen
1129
- */
1130
- fallbackExitFullscreen() {
1131
- this.isFullscreen = false;
1132
- this.zoomora.classList.remove("fullscreen");
1133
- this.updateButtons();
1134
- }
1135
- /**
1136
- * Toggle thumbnails visibility
1137
- */
1138
- toggleThumbnails() {
1139
- if (this.items.length === 1) return;
1140
- this.thumbnailsVisible = !this.thumbnailsVisible;
1141
- if (this.thumbs) {
1142
- this.thumbs.classList.toggle("thumbs-hidden", !this.thumbnailsVisible);
1143
- if (!this.caption || !this.items[this.currentIndex]) return;
1144
- const item = this.items[this.currentIndex];
1145
- const caption = item.dataset.caption || item.title || "";
1146
- if (caption && this.thumbs.classList.contains("thumbs-hidden")) {
1147
- this.caption.style.bottom = "20px";
1148
- this.caption.style.transition = "bottom 0.3s ease, transform 0.3s ease";
1149
- } else {
1150
- this.caption.style.bottom = "120px";
1151
- }
1152
- }
1153
- }
1154
- /**
1155
- * Start drag operation
1156
- * @param {Event} e - Mouse or touch event
1157
- */
1158
- startDrag(e) {
1159
- const media = this.content.querySelector(".zoomora-media");
1160
- if (!media || !this.isZoomed) return;
1161
- this.isDragging = true;
1162
- media.classList.add("dragging");
1163
- media.style.cursor = "grabbing";
1164
- this.startX = e.clientX || e.touches && e.touches[0].clientX || 0;
1165
- this.startY = e.clientY || e.touches && e.touches[0].clientY || 0;
1166
- const transform = window.getComputedStyle(media).transform;
1167
- if (transform && transform !== "none") {
1168
- const matrix = new DOMMatrix(transform);
1169
- this.currentX = matrix.m41;
1170
- this.currentY = matrix.m42;
1171
- } else {
1172
- this.currentX = 0;
1173
- this.currentY = 0;
1174
- }
1175
- e.preventDefault();
1176
- }
1177
- /**
1178
- * Handle drag movement
1179
- * @param {Event} e - Mouse or touch event
1180
- */
1181
- drag(e) {
1182
- if (!this.isDragging) return;
1183
- e.preventDefault();
1184
- const media = this.content.querySelector(".zoomora-media");
1185
- if (!media) return;
1186
- const clientX = e.clientX || e.touches && e.touches[0].clientX || 0;
1187
- const clientY = e.clientY || e.touches && e.touches[0].clientY || 0;
1188
- const deltaX = clientX - this.startX;
1189
- const deltaY = clientY - this.startY;
1190
- let newX = this.currentX + deltaX;
1191
- let newY = this.currentY + deltaY;
1192
- const container = this.content.getBoundingClientRect();
1193
- const mediaRect = media.getBoundingClientRect();
1194
- const overflowX = Math.max(0, (mediaRect.width - container.width) / 2);
1195
- const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
1196
- if (overflowX > 0) {
1197
- newX = Math.max(-overflowX, Math.min(overflowX, newX));
1198
- } else {
1199
- newX = 0;
1200
- }
1201
- if (overflowY > 0) {
1202
- newY = Math.max(-overflowY, Math.min(overflowY, newY));
1203
- } else {
1204
- newY = 0;
1205
- }
1206
- media.style.transform = `translate(${newX}px, ${newY}px) scale(${this.currentScale})`;
1207
- this.startX = clientX;
1208
- this.startY = clientY;
1209
- this.currentX = newX;
1210
- this.currentY = newY;
1211
- }
1212
- /**
1213
- * End drag operation
1214
- */
1215
- endDrag() {
1216
- if (!this.isDragging) return;
1217
- this.isDragging = false;
1218
- this.lastDragTime = Date.now();
1219
- const media = this.content.querySelector(".zoomora-media");
1220
- if (media) {
1221
- media.classList.remove("dragging");
1222
- if (this.isZoomed) {
1223
- media.style.cursor = "grab";
1224
- }
1225
- }
1226
- }
1227
- /**
1228
- * Go to specific item by index
1229
- * @param {number} index - Item index
1230
- */
1231
- goTo(index) {
1232
- if (index < 0 || index >= this.items.length) return;
1233
- this.currentIndex = index;
1234
- this.loadContent();
1235
- this.updateCounter();
1236
- this.updateThumbnails();
1237
- this.updateNavigation();
1238
- this.updateCaption();
1239
- }
1240
- /**
1241
- * Get current item index
1242
- * @returns {number} - Current index
1243
- */
1244
- getCurrentIndex() {
1245
- return this.currentIndex;
1246
- }
1247
- /**
1248
- * Get total number of items
1249
- * @returns {number} - Total items
1250
- */
1251
- getTotalItems() {
1252
- return this.items.length;
1253
- }
1254
- /**
1255
- * Check if lightbox is open
1256
- * @returns {boolean} - True if open
1257
- */
1258
- isOpen() {
1259
- return this.zoomora && this.zoomora.classList.contains("active");
1260
- }
1261
- /**
1262
- * Update options
1263
- * @param {Object} newOptions - New options to merge
1264
- */
1265
- updateOptions(newOptions) {
1266
- this.options = __spreadValues(__spreadValues({}, this.options), newOptions);
1267
- }
1268
- /**
1269
- * Refresh the lightbox (re-scan for items)
1270
- */
1271
- refresh() {
1272
- if (this.isOpen()) {
1273
- const currentItem = this.items[this.currentIndex];
1274
- if (currentItem) {
1275
- this.open(currentItem);
1276
- }
1277
- }
1278
- }
1279
- /**
1280
- * Destroy the lightbox instance
1281
- */
1282
- destroy() {
1283
- if (!this.isInitialized) return;
1284
- if (this.isOpen()) {
1285
- this.close();
1286
- }
1287
- this.stopAutoHideTimer();
1288
- this.eventListeners.forEach((listeners, key) => {
1289
- listeners.forEach(({ element, event, handler, options }) => {
1290
- if (element && element.removeEventListener) {
1291
- element.removeEventListener(event, handler, options);
1292
- }
1293
- });
1294
- });
1295
- this.eventListeners.clear();
1296
- if (this.zoomora && this.zoomora.parentNode) {
1297
- this.zoomora.parentNode.removeChild(this.zoomora);
1298
- }
1299
- this.isInitialized = false;
1300
- this.items = [];
1301
- this.currentIndex = 0;
1302
- this.zoomora = null;
1303
- this.content = null;
1304
- this.counter = null;
1305
- this.thumbs = null;
1306
- this.loading = null;
1307
- this.caption = null;
1308
- }
1309
- }
1310
- return Zoomora;
1311
- });
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
+ })))();
1312
1355
  });
1313
- //# sourceMappingURL=zoomora.umd.js.map
1356
+
1357
+ //# sourceMappingURL=zoomora.umd.js.map