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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zoomora",
3
- "version": "1.2.0",
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.4.4",
43
- "sass": "^1.89.2",
44
- "vite": "^7.0.4"
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
@@ -1,29 +1,27 @@
1
1
  /*!
2
- * Zoomora Lightbox Plugin v1.0.0
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) 2025 Faruk Ahmed (FrontTheme)
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: 'Zoomora';
13
- src: url('fonts/Zoomora.eot?y5poek');
14
- src: url('fonts/Zoomora.eot?y5poek#iefix') format('embedded-opentype'),
15
- url('fonts/Zoomora.ttf?y5poek') format('truetype'),
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
- i[class^="zoomorai-"] {
24
- /* use !important to prevent issues with browser extensions that change fonts */
25
- font-family: 'Zoomora' !important;
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: rgba($dark-color, 0.95);
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: rgba(255, 255, 255, 0.2);
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: rgba(255, 255, 255, 0.3);
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: rgba(255, 255, 255, 0.3);
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 icon
572
+ // Auto-hide button active state
556
573
  #zoomoraAutoHideBtn.active {
557
- color: $primary-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.0
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
- <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
167
- <i class="zoomorai-thumbnails"></i>
168
- </button>
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
- <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
171
- <i class="zoomorai-zoom"></i>
172
- </button>
173
- <button class="zoomora-btn" id="zoomoraFullscreenBtn" title="Fullscreen">
174
- <i class="zoomorai-expand"></i>
175
- </button>
176
- <button class="zoomora-btn" id="zoomoraCollapseBtn" title="Exit Fullscreen" style="display: none;">
177
- <i class="zoomorai-collapse"></i>
178
- </button>
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.addEventListener(document.getElementById('zoomoraZoomBtn'), 'click', () => this.toggleZoom());
235
- this.addEventListener(document.getElementById('zoomoraFullscreenBtn'), 'click', () => this.toggleFullscreen());
236
- this.addEventListener(document.getElementById('zoomoraCollapseBtn'), 'click', () => this.exitFullscreen());
237
- this.addEventListener(document.getElementById('zoomoraThumbnailBtn'), 'click', () => this.toggleThumbnails());
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 {Element} element - DOM element
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 if enabled
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
- if (fitScale * 1.5 <= 1) levels.push(1.5);
1067
- if (fitScale * 2 <= 1) levels.push(2);
1068
- if (fitScale * 3 <= 1) levels.push(3);
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, key) => {
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);