zoomora 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +69 -26
- package/dist/zoomora.css +5 -3
- package/dist/zoomora.es.js +1325 -1286
- package/dist/zoomora.es.js.map +1 -1
- package/dist/zoomora.es.min.js +547 -10
- package/dist/zoomora.umd.js +1326 -1282
- package/dist/zoomora.umd.js.map +1 -1
- package/dist/zoomora.umd.min.js +554 -10
- package/package.json +4 -4
- package/src/styles/fonts/Zoomora.otf +0 -0
- package/src/styles/fonts/Zoomora.ttf +0 -0
- package/src/styles/fonts/Zoomora.woff +0 -0
- package/src/styles/fonts/Zoomora.woff2 +0 -0
- package/src/styles/zoomora.scss +36 -19
- package/src/zoomora.js +181 -59
- package/dist/zoomora.es.min.js.map +0 -1
- package/dist/zoomora.umd.min.js.map +0 -1
- package/src/styles/fonts/Zoomora.eot +0 -0
- package/src/styles/fonts/Zoomora.svg +0 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zoomora",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/zoomora.umd.js",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"clean": "rm -rf dist"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@rollup/plugin-terser": "^0.
|
|
43
|
-
"sass": "^1.
|
|
44
|
-
"vite": "^
|
|
42
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
43
|
+
"sass": "^1.101.0",
|
|
44
|
+
"vite": "^8.1.2"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/styles/zoomora.scss
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Zoomora Lightbox Plugin v1.
|
|
2
|
+
* Zoomora Lightbox Plugin v1.3.0
|
|
3
3
|
* A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
|
|
4
4
|
*
|
|
5
|
-
* Copyright (c)
|
|
5
|
+
* Copyright (c) 2026 Faruk Ahmed (FrontTheme)
|
|
6
6
|
* Licensed under MIT License
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
@use 'variables' as *;
|
|
10
10
|
|
|
11
|
+
// Font icons
|
|
11
12
|
@font-face {
|
|
12
|
-
font-family:
|
|
13
|
-
src: url(
|
|
14
|
-
|
|
15
|
-
url(
|
|
16
|
-
url('fonts/Zoomora.woff?y5poek') format('woff'),
|
|
17
|
-
url('fonts/Zoomora.svg?y5poek#Zoomora') format('svg');
|
|
13
|
+
font-family: "Zoomora";
|
|
14
|
+
src: url("fonts/Zoomora.woff2") format("woff2"),
|
|
15
|
+
url("fonts/Zoomora.ttf") format("truetype"),
|
|
16
|
+
url("fonts/Zoomora.woff") format("woff");
|
|
18
17
|
font-weight: normal;
|
|
19
18
|
font-style: normal;
|
|
20
19
|
font-display: block;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
/*
|
|
25
|
-
font-family:
|
|
26
|
-
speak: never;
|
|
22
|
+
.zoomorai, [class^="zoomorai-"], [class*=" zoomorai-"] {
|
|
23
|
+
/* Use !important to prevent extensions from overriding this font. */
|
|
24
|
+
font-family: "Zoomora" !important;
|
|
27
25
|
font-style: normal;
|
|
28
26
|
font-weight: normal;
|
|
29
27
|
font-variant: normal;
|
|
@@ -71,13 +69,33 @@ i[class^="zoomorai-"] {
|
|
|
71
69
|
content: "\e908";
|
|
72
70
|
}
|
|
73
71
|
|
|
72
|
+
// ─── Runtime CSS custom properties ───────────────────────────────────────────
|
|
73
|
+
// Sass variables are compiled away at build time; these :root declarations
|
|
74
|
+
// expose the same values as overridable runtime variables so users can theme
|
|
75
|
+
// Zoomora with plain CSS without recompiling the Sass source.
|
|
76
|
+
//
|
|
77
|
+
// :root {
|
|
78
|
+
// --zoomora-overlay-bg: rgba(0, 0, 0, 0.92);
|
|
79
|
+
// --zoomora-control-bg: rgba(255, 255, 255, 0.15);
|
|
80
|
+
// --zoomora-control-hover: rgba(255, 255, 255, 0.25);
|
|
81
|
+
// --zoomora-primary: #ff6b6b;
|
|
82
|
+
// }
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
:root {
|
|
85
|
+
--zoomora-overlay-bg: #{rgba($dark-color, 0.95)};
|
|
86
|
+
--zoomora-control-bg: rgba(255, 255, 255, 0.2);
|
|
87
|
+
--zoomora-control-hover: rgba(255, 255, 255, 0.3);
|
|
88
|
+
--zoomora-primary: #{$primary-color};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//Zoomora Lightbox Styles
|
|
74
92
|
.zoomora {
|
|
75
93
|
position: fixed;
|
|
76
94
|
top: 0;
|
|
77
95
|
left: 0;
|
|
78
96
|
width: 100%;
|
|
79
97
|
height: 100%;
|
|
80
|
-
background:
|
|
98
|
+
background: var(--zoomora-overlay-bg);
|
|
81
99
|
backdrop-filter: blur(20px);
|
|
82
100
|
z-index: 10000;
|
|
83
101
|
display: flex;
|
|
@@ -142,7 +160,7 @@ i[class^="zoomorai-"] {
|
|
|
142
160
|
height: 50px;
|
|
143
161
|
border: none;
|
|
144
162
|
border-radius: 50%;
|
|
145
|
-
background:
|
|
163
|
+
background: var(--zoomora-control-bg);
|
|
146
164
|
color: white;
|
|
147
165
|
cursor: pointer;
|
|
148
166
|
display: flex;
|
|
@@ -154,7 +172,7 @@ i[class^="zoomorai-"] {
|
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
.zoomora-btn:hover {
|
|
157
|
-
background:
|
|
175
|
+
background: var(--zoomora-control-hover);
|
|
158
176
|
transform: scale(1.1);
|
|
159
177
|
}
|
|
160
178
|
|
|
@@ -252,7 +270,7 @@ iframe.zoomora-video {
|
|
|
252
270
|
}
|
|
253
271
|
|
|
254
272
|
.zoomora-nav:hover {
|
|
255
|
-
background:
|
|
273
|
+
background: var(--zoomora-control-hover);
|
|
256
274
|
transform: translateY(-50%) scale(1.1);
|
|
257
275
|
}
|
|
258
276
|
|
|
@@ -331,7 +349,6 @@ iframe.zoomora-video {
|
|
|
331
349
|
pointer-events: none;
|
|
332
350
|
}
|
|
333
351
|
|
|
334
|
-
|
|
335
352
|
.zoomora-thumb img {
|
|
336
353
|
width: 100%;
|
|
337
354
|
height: 100%;
|
|
@@ -552,7 +569,7 @@ iframe.zoomora-video {
|
|
|
552
569
|
background-color: rgba(255, 255, 255, 0.2);
|
|
553
570
|
}
|
|
554
571
|
|
|
555
|
-
// Auto-hide button
|
|
572
|
+
// Auto-hide button active state
|
|
556
573
|
#zoomoraAutoHideBtn.active {
|
|
557
|
-
color:
|
|
574
|
+
color: var(--zoomora-primary);
|
|
558
575
|
}
|
package/src/zoomora.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Zoomora Lightbox Plugin v1.
|
|
2
|
+
* Zoomora Lightbox Plugin v1.3.0
|
|
3
3
|
* A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2026 Faruk Ahmed (FrontTheme)
|
|
@@ -84,11 +84,8 @@ import './styles/zoomora.scss';
|
|
|
84
84
|
this.autoHideTimeout = null;
|
|
85
85
|
this.autoHideDelay = 3000;
|
|
86
86
|
this.isControlsVisible = true;
|
|
87
|
-
this.idleTimer = null;
|
|
88
87
|
|
|
89
88
|
this.lastDragTime = 0;
|
|
90
|
-
|
|
91
|
-
// Bound methods for event listeners
|
|
92
89
|
this.boundMethods = {
|
|
93
90
|
handleDocumentClick: this.handleDocumentClick.bind(this),
|
|
94
91
|
handleKeydown: this.handleKeydown.bind(this),
|
|
@@ -101,6 +98,7 @@ import './styles/zoomora.scss';
|
|
|
101
98
|
handleTouchStart: this.handleTouchStart.bind(this),
|
|
102
99
|
handleTouchMove: this.handleTouchMove.bind(this),
|
|
103
100
|
handleTouchEnd: this.handleTouchEnd.bind(this),
|
|
101
|
+
handleWheel: this.handleWheel.bind(this),
|
|
104
102
|
};
|
|
105
103
|
|
|
106
104
|
this.init();
|
|
@@ -161,21 +159,27 @@ import './styles/zoomora.scss';
|
|
|
161
159
|
<div class="zoomora" id="zoomoraLightbox">
|
|
162
160
|
<div class="zoomora-container">
|
|
163
161
|
<div class="zoomora-header" id="zoomoraHeader">
|
|
164
|
-
<div class="zoomora-counter" id="zoomoraCounter">1 / 1</div>
|
|
162
|
+
<div class="zoomora-counter" id="zoomoraCounter" ${!this.options.showCounter ? 'style="opacity:0;visibility:hidden;"' : ''}>1 / 1</div>
|
|
165
163
|
<div class="zoomora-controls">
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
164
|
+
${this.options.showThumbnails ? `
|
|
165
|
+
<button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
|
|
166
|
+
<i class="zoomorai-thumbnails"></i>
|
|
167
|
+
</button>
|
|
168
|
+
` : ''}
|
|
169
169
|
${autoHideButtonHTML}
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
${this.options.showZoom ? `
|
|
171
|
+
<button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
|
|
172
|
+
<i class="zoomorai-zoom"></i>
|
|
173
|
+
</button>
|
|
174
|
+
` : ''}
|
|
175
|
+
${this.options.showFullscreen ? `
|
|
176
|
+
<button class="zoomora-btn" id="zoomoraFullscreenBtn" title="Fullscreen">
|
|
177
|
+
<i class="zoomorai-expand"></i>
|
|
178
|
+
</button>
|
|
179
|
+
<button class="zoomora-btn" id="zoomoraCollapseBtn" title="Exit Fullscreen" style="display: none;">
|
|
180
|
+
<i class="zoomorai-collapse"></i>
|
|
181
|
+
</button>
|
|
182
|
+
` : ''}
|
|
179
183
|
<button class="zoomora-btn" id="zoomoraCloseBtn" title="Close">
|
|
180
184
|
<i class="zoomorai-close"></i>
|
|
181
185
|
</button>
|
|
@@ -194,8 +198,8 @@ import './styles/zoomora.scss';
|
|
|
194
198
|
<i class="zoomorai-arrow-right"></i>
|
|
195
199
|
</button>
|
|
196
200
|
|
|
197
|
-
<div class="zoomora-caption" id="zoomoraCaption"></div>
|
|
198
|
-
<div class="zoomora-thumbnails" id="zoomoraThumbs"></div>
|
|
201
|
+
<div class="zoomora-caption" id="zoomoraCaption" ${!this.options.showThumbnails ? 'style="bottom:20px;"' : ''}></div>
|
|
202
|
+
<div class="zoomora-thumbnails" id="zoomoraThumbs" ${!this.options.showThumbnails ? 'style="display:none;"' : ''}></div>
|
|
199
203
|
</div>
|
|
200
204
|
</div>
|
|
201
205
|
`;
|
|
@@ -231,10 +235,16 @@ import './styles/zoomora.scss';
|
|
|
231
235
|
this.addEventListener(document.getElementById('zoomoraCloseBtn'), 'click', () => this.close());
|
|
232
236
|
this.addEventListener(document.getElementById('zoomoraPrevBtn'), 'click', () => this.prev());
|
|
233
237
|
this.addEventListener(document.getElementById('zoomoraNextBtn'), 'click', () => this.next());
|
|
234
|
-
this.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.
|
|
238
|
+
if (this.options.showZoom) {
|
|
239
|
+
this.addEventListener(document.getElementById('zoomoraZoomBtn'), 'click', () => this.toggleZoom());
|
|
240
|
+
}
|
|
241
|
+
if (this.options.showFullscreen) {
|
|
242
|
+
this.addEventListener(document.getElementById('zoomoraFullscreenBtn'), 'click', () => this.toggleFullscreen());
|
|
243
|
+
this.addEventListener(document.getElementById('zoomoraCollapseBtn'), 'click', () => this.exitFullscreen());
|
|
244
|
+
}
|
|
245
|
+
if (this.options.showThumbnails) {
|
|
246
|
+
this.addEventListener(document.getElementById('zoomoraThumbnailBtn'), 'click', () => this.toggleThumbnails());
|
|
247
|
+
}
|
|
238
248
|
|
|
239
249
|
// Fullscreen change events
|
|
240
250
|
this.addEventListener(document, 'fullscreenchange', this.boundMethods.handleFullscreenChange);
|
|
@@ -262,6 +272,11 @@ import './styles/zoomora.scss';
|
|
|
262
272
|
this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
|
|
263
273
|
this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
|
|
264
274
|
|
|
275
|
+
// Scroll-wheel zoom
|
|
276
|
+
if (this.options.showZoom) {
|
|
277
|
+
this.addEventListener(this.content, 'wheel', this.boundMethods.handleWheel, {passive: false});
|
|
278
|
+
}
|
|
279
|
+
|
|
265
280
|
// Add auto-hide toggle button handler (only if enabled)
|
|
266
281
|
if (this.options.showAutoHideToggle) {
|
|
267
282
|
this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
|
|
@@ -275,7 +290,7 @@ import './styles/zoomora.scss';
|
|
|
275
290
|
|
|
276
291
|
/**
|
|
277
292
|
* Add event listener with cleanup tracking
|
|
278
|
-
* @param {
|
|
293
|
+
* @param {EventTarget} element - DOM element or document
|
|
279
294
|
* @param {string} event - Event type
|
|
280
295
|
* @param {Function} handler - Event handler
|
|
281
296
|
* @param {Object} options - Event options
|
|
@@ -489,6 +504,54 @@ import './styles/zoomora.scss';
|
|
|
489
504
|
this.endDrag();
|
|
490
505
|
}
|
|
491
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Handle scroll-wheel zoom
|
|
509
|
+
* @param {WheelEvent} e - Wheel event
|
|
510
|
+
*/
|
|
511
|
+
handleWheel(e) {
|
|
512
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
513
|
+
if (!media || media.tagName !== 'IMG') return;
|
|
514
|
+
|
|
515
|
+
const zoomConfig = this.calculateZoomLevels(media);
|
|
516
|
+
if (!zoomConfig.canZoom) return;
|
|
517
|
+
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
|
|
520
|
+
const maxScale = zoomConfig.levels[zoomConfig.levels.length - 1];
|
|
521
|
+
const step = this.options.zoomStep || 0.1;
|
|
522
|
+
const direction = e.deltaY < 0 ? 1 : -1; // scroll/pinch up = zoom in
|
|
523
|
+
|
|
524
|
+
let newScale = (this.currentScale || 1) + (direction * step * maxScale);
|
|
525
|
+
newScale = Math.max(1, Math.min(maxScale, newScale));
|
|
526
|
+
|
|
527
|
+
if (newScale === this.currentScale) return;
|
|
528
|
+
|
|
529
|
+
this.currentScale = newScale;
|
|
530
|
+
this.isZoomed = newScale > 1;
|
|
531
|
+
this.currentX = 0;
|
|
532
|
+
this.currentY = 0;
|
|
533
|
+
|
|
534
|
+
if (this.isZoomed) {
|
|
535
|
+
media.style.transform = `scale(${newScale})`;
|
|
536
|
+
media.style.cursor = 'grab';
|
|
537
|
+
media.classList.add('zoomed');
|
|
538
|
+
} else {
|
|
539
|
+
media.style.transform = '';
|
|
540
|
+
media.style.cursor = 'pointer';
|
|
541
|
+
media.classList.remove('zoomed');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Keep the click-to-cycle zoom index roughly in sync with the
|
|
545
|
+
// continuous wheel scale, so a click after scrolling cycles sensibly.
|
|
546
|
+
let nearestIndex = 0;
|
|
547
|
+
zoomConfig.levels.forEach((level, index) => {
|
|
548
|
+
if (Math.abs(level - newScale) < Math.abs(zoomConfig.levels[nearestIndex] - newScale)) {
|
|
549
|
+
nearestIndex = index;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
media.dataset.zoomLevel = nearestIndex.toString();
|
|
553
|
+
}
|
|
554
|
+
|
|
492
555
|
/**
|
|
493
556
|
* Get image source from element
|
|
494
557
|
* @param {Element} item - DOM element
|
|
@@ -623,6 +686,79 @@ import './styles/zoomora.scss';
|
|
|
623
686
|
this.items = Array.from(document.querySelectorAll(groupSelector));
|
|
624
687
|
this.currentIndex = Math.max(0, this.items.indexOf(element));
|
|
625
688
|
|
|
689
|
+
this._activateLightbox();
|
|
690
|
+
|
|
691
|
+
// Trigger callback
|
|
692
|
+
if (typeof this.options.onOpen === 'function') {
|
|
693
|
+
this.options.onOpen(element, this.currentIndex);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Open lightbox programmatically with image(s) not present in the DOM.
|
|
699
|
+
* Accepts a single URL, an array of URLs, a config object, or an array
|
|
700
|
+
* of config objects — so all of these work:
|
|
701
|
+
*
|
|
702
|
+
* lightbox.openImage('photo.jpg');
|
|
703
|
+
* lightbox.openImage(['a.jpg', 'b.jpg'], { startIndex: 1 });
|
|
704
|
+
* lightbox.openImage({ src: 'photo.jpg', caption: 'Hello' });
|
|
705
|
+
* lightbox.openImage([
|
|
706
|
+
* { src: 'a.jpg', caption: 'First' },
|
|
707
|
+
* { src: 'https://youtu.be/xyz', type: 'video', thumb: 'poster.jpg' },
|
|
708
|
+
* ]);
|
|
709
|
+
*
|
|
710
|
+
* @param {string|string[]|Object|Object[]} images - URL(s) or config object(s)
|
|
711
|
+
* @param {Object} [opts] - Options
|
|
712
|
+
* @param {number} [opts.startIndex=0] - Index to open at when passing an array
|
|
713
|
+
*/
|
|
714
|
+
openImage(images, opts = {}) {
|
|
715
|
+
const list = Array.isArray(images) ? images : [images];
|
|
716
|
+
|
|
717
|
+
this.items = list.map(entry =>
|
|
718
|
+
this.createVirtualItem(typeof entry === 'string' ? {src: entry} : entry)
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
this.currentIndex = Math.max(
|
|
722
|
+
0,
|
|
723
|
+
Math.min(opts.startIndex || 0, this.items.length - 1)
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
this._activateLightbox();
|
|
727
|
+
|
|
728
|
+
// Trigger callback
|
|
729
|
+
if (typeof this.options.onOpen === 'function') {
|
|
730
|
+
this.options.onOpen(this.items[this.currentIndex], this.currentIndex);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Create a virtual item that mimics a DOM element's interface so
|
|
736
|
+
* programmatic images work through the existing render pipeline without
|
|
737
|
+
* any changes to getImageSource, getThumbnailSource, updateCaption, etc.
|
|
738
|
+
* @param {Object} config
|
|
739
|
+
* @param {string} config.src - Full-size image/video/iframe URL
|
|
740
|
+
* @param {string} [config.thumb] - Thumbnail URL (falls back to src)
|
|
741
|
+
* @param {string} [config.type='image'] - 'image', 'video', or 'iframe'
|
|
742
|
+
* @param {string} [config.caption=''] - Caption text
|
|
743
|
+
* @param {string} [config.alt=''] - Alt text for the thumbnail img
|
|
744
|
+
* @returns {Object} Virtual item
|
|
745
|
+
*/
|
|
746
|
+
createVirtualItem({src, thumb, type = 'image', caption = '', alt = ''}) {
|
|
747
|
+
const thumbSrc = thumb || src;
|
|
748
|
+
return {
|
|
749
|
+
dataset: {src, type, caption},
|
|
750
|
+
src: thumbSrc,
|
|
751
|
+
alt,
|
|
752
|
+
title: caption,
|
|
753
|
+
getAttribute: (name) => name === 'src' ? thumbSrc : null,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Shared activation sequence used by both open() and openImage().
|
|
759
|
+
* Resets state, shows the lightbox, and triggers all UI updates.
|
|
760
|
+
*/
|
|
761
|
+
_activateLightbox() {
|
|
626
762
|
// Reset states
|
|
627
763
|
this.isZoomed = false;
|
|
628
764
|
this.isFullscreen = false;
|
|
@@ -641,17 +777,12 @@ import './styles/zoomora.scss';
|
|
|
641
777
|
this.updateCaption();
|
|
642
778
|
this.updateButtons();
|
|
643
779
|
|
|
644
|
-
// Initialize auto-hide
|
|
780
|
+
// Initialize auto-hide
|
|
645
781
|
if (this.autoHideEnabled) {
|
|
646
782
|
this.startAutoHideTimer();
|
|
647
783
|
} else {
|
|
648
784
|
this.showControls();
|
|
649
785
|
}
|
|
650
|
-
|
|
651
|
-
// Trigger callback
|
|
652
|
-
if (typeof this.options.onOpen === 'function') {
|
|
653
|
-
this.options.onOpen(element, this.currentIndex);
|
|
654
|
-
}
|
|
655
786
|
}
|
|
656
787
|
|
|
657
788
|
/**
|
|
@@ -1017,11 +1148,11 @@ import './styles/zoomora.scss';
|
|
|
1017
1148
|
const maxZoomRatio = Math.max(zoomRatioX, zoomRatioY);
|
|
1018
1149
|
|
|
1019
1150
|
// Store zoom info
|
|
1020
|
-
img.dataset.zoomRatio = maxZoomRatio;
|
|
1021
|
-
img.dataset.naturalWidth = naturalWidth;
|
|
1022
|
-
img.dataset.naturalHeight = naturalHeight;
|
|
1023
|
-
img.dataset.renderedWidth = renderedWidth;
|
|
1024
|
-
img.dataset.renderedHeight = renderedHeight;
|
|
1151
|
+
img.dataset.zoomRatio = maxZoomRatio.toString();
|
|
1152
|
+
img.dataset.naturalWidth = naturalWidth.toString();
|
|
1153
|
+
img.dataset.naturalHeight = naturalHeight.toString();
|
|
1154
|
+
img.dataset.renderedWidth = renderedWidth.toString();
|
|
1155
|
+
img.dataset.renderedHeight = renderedHeight.toString();
|
|
1025
1156
|
|
|
1026
1157
|
// Enable zoom if significantly scaled down
|
|
1027
1158
|
if (isScaledDown && maxZoomRatio > 1.1) {
|
|
@@ -1059,16 +1190,24 @@ import './styles/zoomora.scss';
|
|
|
1059
1190
|
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1060
1191
|
}
|
|
1061
1192
|
|
|
1193
|
+
// Respect the configured ceiling (falls back to 3x only if misconfigured)
|
|
1194
|
+
const configuredMax = Number(this.options.maxZoomScale);
|
|
1195
|
+
const maxZoom = Number.isFinite(configuredMax) && configuredMax > 0
|
|
1196
|
+
? Math.max(1, configuredMax)
|
|
1197
|
+
: 3;
|
|
1198
|
+
|
|
1062
1199
|
// Calculate zoom levels based on FIT scale (not current scale)
|
|
1063
1200
|
const levels = [1]; // Start with fit-to-screen
|
|
1064
1201
|
|
|
1065
|
-
// Add progressive levels: 1.5x, 2x, 3x
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1202
|
+
// Add progressive levels up to the configured max: 1.5x, 2x, 3x, 4x, 5x...
|
|
1203
|
+
[1.5, 2, 3, 4, 5].forEach((candidate) => {
|
|
1204
|
+
if (candidate <= maxZoom && fitScale * candidate <= 1) {
|
|
1205
|
+
levels.push(candidate);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1069
1208
|
|
|
1070
|
-
// Add actual size (100%) as final level
|
|
1071
|
-
const actualSizeScale = 1 / fitScale;
|
|
1209
|
+
// Add actual size (100%) as final level, capped at the configured max
|
|
1210
|
+
const actualSizeScale = Math.min(1 / fitScale, maxZoom);
|
|
1072
1211
|
if (actualSizeScale > levels[levels.length - 1]) {
|
|
1073
1212
|
levels.push(actualSizeScale);
|
|
1074
1213
|
}
|
|
@@ -1251,23 +1390,6 @@ import './styles/zoomora.scss';
|
|
|
1251
1390
|
}
|
|
1252
1391
|
}
|
|
1253
1392
|
|
|
1254
|
-
/**
|
|
1255
|
-
* Update image transform based on current state
|
|
1256
|
-
* @param {HTMLElement} media - Media element
|
|
1257
|
-
*/
|
|
1258
|
-
updateImageTransform(media) {
|
|
1259
|
-
if (!media) return;
|
|
1260
|
-
|
|
1261
|
-
if (this.currentScale === 1) {
|
|
1262
|
-
media.style.transform = '';
|
|
1263
|
-
} else {
|
|
1264
|
-
// IMPORTANT: translate in scaled space, so divide by scale
|
|
1265
|
-
const translateX = this.currentX / this.currentScale;
|
|
1266
|
-
const translateY = this.currentY / this.currentScale;
|
|
1267
|
-
media.style.transform = `scale(${this.currentScale}) translate(${translateX}px, ${translateY}px)`;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1393
|
/**
|
|
1272
1394
|
* Toggle fullscreen mode
|
|
1273
1395
|
*/
|
|
@@ -1609,7 +1731,7 @@ import './styles/zoomora.scss';
|
|
|
1609
1731
|
this.stopAutoHideTimer();
|
|
1610
1732
|
|
|
1611
1733
|
// Remove all event listeners
|
|
1612
|
-
this.eventListeners.forEach((listeners,
|
|
1734
|
+
this.eventListeners.forEach((listeners, _key) => {
|
|
1613
1735
|
listeners.forEach(({element, event, handler, options}) => {
|
|
1614
1736
|
if (element && element.removeEventListener) {
|
|
1615
1737
|
element.removeEventListener(event, handler, options);
|