zoomora 1.2.1 → 1.3.0

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