zoomora 1.0.0 → 1.1.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/src/zoomora.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Zoomora Lightbox Plugin v1.0.0
2
+ * Zoomora Lightbox Plugin v1.1.0
3
3
  * A modern, responsive lightbox plugin with zoom, fullscreen, and gallery features
4
4
  *
5
5
  * Copyright (c) 2025 Faruk Ahmed (FrontTheme)
@@ -85,6 +85,8 @@ import './styles/zoomora.scss';
85
85
  this.isControlsVisible = true;
86
86
  this.idleTimer = null;
87
87
 
88
+ this.lastDragTime = 0;
89
+
88
90
  // Bound methods for event listeners
89
91
  this.boundMethods = {
90
92
  handleDocumentClick: this.handleDocumentClick.bind(this),
@@ -147,6 +149,13 @@ import './styles/zoomora.scss';
147
149
  return;
148
150
  }
149
151
 
152
+ // Build auto-hide button conditionally
153
+ const autoHideButtonHTML = this.options.showAutoHideToggle ? `
154
+ <button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
155
+ <i class="zoomorai-controls-off"></i>
156
+ </button>
157
+ ` : '';
158
+
150
159
  const zoomoraHTML = `
151
160
  <div class="zoomora" id="zoomoraLightbox">
152
161
  <div class="zoomora-container">
@@ -156,9 +165,7 @@ import './styles/zoomora.scss';
156
165
  <button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
157
166
  <i class="zoomorai-thumbnails"></i>
158
167
  </button>
159
- <button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
160
- <i class="zoomorai-controls-off"></i>
161
- </button>
168
+ ${autoHideButtonHTML}
162
169
  <button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
163
170
  <i class="zoomorai-zoom"></i>
164
171
  </button>
@@ -254,13 +261,15 @@ import './styles/zoomora.scss';
254
261
  this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
255
262
  this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
256
263
 
257
- // Add auto-hide toggle button handler
258
- this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
264
+ // Add auto-hide toggle button handler (only if enabled)
265
+ if (this.options.showAutoHideToggle) {
266
+ this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
259
267
 
260
- // Add mouse/touch activity listeners for auto-hide
261
- this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
262
- this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
263
- this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
268
+ // Add mouse/touch activity listeners for auto-hide
269
+ this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
270
+ this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
271
+ this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
272
+ }
264
273
  }
265
274
 
266
275
  /**
@@ -362,8 +371,13 @@ import './styles/zoomora.scss';
362
371
  * @param {Event} e - Click event
363
372
  */
364
373
  handleContentClick(e) {
374
+ // Ignore click if we just finished dragging (within 200ms)
375
+ if (Date.now() - this.lastDragTime < 200) {
376
+ return;
377
+ }
378
+
365
379
  const media = e.target.closest('.zoomora-media');
366
- if (media && !media.classList.contains('no-zoom') && !this.isDragging) {
380
+ if (media && !media.classList.contains('no-zoom')) {
367
381
  this.toggleZoom();
368
382
  }
369
383
  }
@@ -668,16 +682,19 @@ import './styles/zoomora.scss';
668
682
  return;
669
683
  }
670
684
 
685
+ // Reset zoom state
686
+ this.isZoomed = false;
687
+ this.currentX = 0;
688
+ this.currentY = 0;
689
+ this.currentScale = 1;
690
+
671
691
  // Show loading spinner
672
692
  this.loading.style.display = 'block';
673
693
 
674
- // Clear content but keep the loading reference intact
675
- // Don't replace innerHTML, just clear the slides
676
694
  if (this.slidesContainer) {
677
695
  this.slidesContainer.innerHTML = '';
678
696
  }
679
697
 
680
- // Add slight delay for smooth transition
681
698
  setTimeout(() => {
682
699
  this.renderContent(src, type, item);
683
700
  }, 100);
@@ -953,6 +970,63 @@ import './styles/zoomora.scss';
953
970
  }
954
971
  }
955
972
 
973
+ /**
974
+ * Calculate zoom levels for an image
975
+ * @param {HTMLImageElement} img - Image element
976
+ * @returns {Object} Zoom configuration
977
+ */
978
+ /**
979
+ * Calculate zoom levels for an image
980
+ * @param {HTMLImageElement} img - Image element
981
+ * @returns {Object} Zoom configuration
982
+ */
983
+ calculateZoomLevels(img) {
984
+ if (!img || !img.naturalWidth || !img.naturalHeight) {
985
+ return {canZoom: false, levels: [], currentLevel: 0};
986
+ }
987
+
988
+ // Get CONTAINER size (not current displayed image size)
989
+ const container = this.content.getBoundingClientRect();
990
+ const containerWidth = container.width;
991
+ const containerHeight = container.height;
992
+
993
+ const naturalWidth = img.naturalWidth;
994
+ const naturalHeight = img.naturalHeight;
995
+
996
+ // Calculate what the ORIGINAL fit-to-screen scale would be
997
+ const scaleToFitWidth = containerWidth / naturalWidth;
998
+ const scaleToFitHeight = containerHeight / naturalHeight;
999
+ const fitScale = Math.min(scaleToFitWidth, scaleToFitHeight);
1000
+
1001
+ // If image naturally fits or is smaller than container, can't zoom
1002
+ if (fitScale >= 0.95) {
1003
+ return {canZoom: false, levels: [], currentLevel: 0};
1004
+ }
1005
+
1006
+ // Calculate zoom levels based on FIT scale (not current scale)
1007
+ const levels = [1]; // Start with fit-to-screen
1008
+
1009
+ // Add progressive levels: 1.5x, 2x, 3x
1010
+ if (fitScale * 1.5 <= 1) levels.push(1.5);
1011
+ if (fitScale * 2 <= 1) levels.push(2);
1012
+ if (fitScale * 3 <= 1) levels.push(3);
1013
+
1014
+ // Add actual size (100%) as final level
1015
+ const actualSizeScale = 1 / fitScale;
1016
+ if (actualSizeScale > levels[levels.length - 1]) {
1017
+ levels.push(actualSizeScale);
1018
+ }
1019
+
1020
+ return {
1021
+ canZoom: levels.length > 1,
1022
+ levels: levels,
1023
+ currentLevel: 0,
1024
+ baseScale: fitScale,
1025
+ naturalWidth: naturalWidth,
1026
+ naturalHeight: naturalHeight
1027
+ };
1028
+ }
1029
+
956
1030
  /**
957
1031
  * Update counter display
958
1032
  */
@@ -1071,29 +1145,73 @@ import './styles/zoomora.scss';
1071
1145
  */
1072
1146
  toggleZoom() {
1073
1147
  const media = this.content.querySelector('.zoomora-media');
1074
- if (!media || media.tagName !== 'IMG' || media.classList.contains('no-zoom')) return;
1148
+ if (!media || media.tagName !== 'IMG') return;
1075
1149
 
1076
- this.isZoomed = !this.isZoomed;
1077
- media.classList.toggle('zoomed', this.isZoomed);
1150
+ // Calculate zoom levels
1151
+ const zoomConfig = this.calculateZoomLevels(media);
1078
1152
 
1079
- if (this.isZoomed) {
1080
- const zoomRatio = parseFloat(media.dataset.zoomRatio) || 2;
1081
- let zoomScale = Math.min(zoomRatio, this.options.maxZoomScale);
1153
+ if (!zoomConfig.canZoom) {
1154
+ console.log('Image cannot be zoomed (already at full size or larger)');
1155
+ return;
1156
+ }
1082
1157
 
1083
- if (zoomRatio > 2.5) {
1084
- zoomScale = Math.min(zoomRatio * 0.7, 2.5);
1085
- }
1158
+ // Get or initialize current zoom level
1159
+ if (!media.dataset.zoomLevel) {
1160
+ media.dataset.zoomLevel = '0';
1161
+ }
1086
1162
 
1087
- media.style.transform = `scale(${zoomScale})`;
1088
- media.style.cursor = 'grab';
1089
- media.dataset.currentZoomScale = zoomScale;
1090
- this.currentScale = zoomScale;
1091
- } else {
1163
+ let currentLevel = parseInt(media.dataset.zoomLevel) || 0;
1164
+
1165
+ // Cycle to next level
1166
+ currentLevel++;
1167
+ if (currentLevel >= zoomConfig.levels.length) {
1168
+ // Reset to fit-to-screen
1169
+ currentLevel = 0;
1170
+ }
1171
+
1172
+ const targetScale = zoomConfig.levels[currentLevel];
1173
+ media.dataset.zoomLevel = currentLevel.toString();
1174
+
1175
+ if (currentLevel === 0) {
1176
+ // Back to fit-to-screen
1177
+ this.isZoomed = false;
1178
+ this.isDragging = false;
1179
+ media.classList.remove('dragging');
1092
1180
  media.style.transform = '';
1093
- media.style.cursor = '';
1181
+ media.style.cursor = 'pointer';
1182
+ media.classList.remove('zoomed');
1094
1183
  this.currentX = 0;
1095
1184
  this.currentY = 0;
1096
1185
  this.currentScale = 1;
1186
+ } else {
1187
+ // Zoom in - reset position to center
1188
+ this.isZoomed = true;
1189
+ this.currentScale = targetScale;
1190
+ this.currentX = 0; // Always reset to center on zoom change
1191
+ this.currentY = 0;
1192
+
1193
+ media.style.transform = `translate(0px, 0px) scale(${targetScale})`;
1194
+ media.style.cursor = 'grab';
1195
+ media.classList.add('zoomed');
1196
+
1197
+ console.log(`Zoomed to level ${currentLevel}/${zoomConfig.levels.length - 1} (${(targetScale * 100).toFixed(0)}%)`);
1198
+ }
1199
+ }
1200
+
1201
+ /**
1202
+ * Update image transform based on current state
1203
+ * @param {HTMLElement} media - Media element
1204
+ */
1205
+ updateImageTransform(media) {
1206
+ if (!media) return;
1207
+
1208
+ if (this.currentScale === 1) {
1209
+ media.style.transform = '';
1210
+ } else {
1211
+ // IMPORTANT: translate in scaled space, so divide by scale
1212
+ const translateX = this.currentX / this.currentScale;
1213
+ const translateY = this.currentY / this.currentScale;
1214
+ media.style.transform = `scale(${this.currentScale}) translate(${translateX}px, ${translateY}px)`;
1097
1215
  }
1098
1216
  }
1099
1217
 
@@ -1215,19 +1333,22 @@ import './styles/zoomora.scss';
1215
1333
  if (!media || !this.isZoomed) return;
1216
1334
 
1217
1335
  this.isDragging = true;
1336
+ media.classList.add('dragging');
1218
1337
  media.style.cursor = 'grabbing';
1219
1338
 
1220
1339
  this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1221
1340
  this.startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1222
1341
 
1223
- // Parse current transform
1224
- const transform = media.style.transform;
1225
- const scaleMatch = transform.match(/scale\(([^)]+)\)/);
1226
- const translateMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
1227
-
1228
- this.currentScale = scaleMatch ? parseFloat(scaleMatch[1]) : 1;
1229
- this.currentX = translateMatch ? parseFloat(translateMatch[1]) : 0;
1230
- this.currentY = translateMatch ? parseFloat(translateMatch[2]) : 0;
1342
+ // Read current position from transform
1343
+ const transform = window.getComputedStyle(media).transform;
1344
+ if (transform && transform !== 'none') {
1345
+ const matrix = new DOMMatrix(transform);
1346
+ this.currentX = matrix.m41;
1347
+ this.currentY = matrix.m42;
1348
+ } else {
1349
+ this.currentX = 0;
1350
+ this.currentY = 0;
1351
+ }
1231
1352
 
1232
1353
  e.preventDefault();
1233
1354
  }
@@ -1238,29 +1359,49 @@ import './styles/zoomora.scss';
1238
1359
  */
1239
1360
  drag(e) {
1240
1361
  if (!this.isDragging) return;
1241
-
1242
1362
  e.preventDefault();
1363
+
1364
+ const media = this.content.querySelector('.zoomora-media');
1365
+ if (!media) return;
1366
+
1243
1367
  const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
1244
1368
  const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
1245
1369
 
1246
1370
  const deltaX = clientX - this.startX;
1247
1371
  const deltaY = clientY - this.startY;
1248
1372
 
1249
- const newX = this.currentX + deltaX;
1250
- const newY = this.currentY + deltaY;
1251
-
1252
- const media = this.content.querySelector('.zoomora-media');
1253
- if (!media) return;
1373
+ let newX = this.currentX + deltaX;
1374
+ let newY = this.currentY + deltaY;
1254
1375
 
1376
+ // Calculate boundaries for BOTH width AND height
1377
+ const container = this.content.getBoundingClientRect();
1255
1378
  const mediaRect = media.getBoundingClientRect();
1256
- const scale = this.currentScale;
1257
- const maxX = (mediaRect.width * (scale - 1)) / 2;
1258
- const maxY = (mediaRect.height * (scale - 1)) / 2;
1259
1379
 
1260
- const constrainedX = Math.max(-maxX, Math.min(maxX, newX));
1261
- const constrainedY = Math.max(-maxY, Math.min(maxY, newY));
1380
+ // How much the scaled image overflows the container
1381
+ const overflowX = Math.max(0, (mediaRect.width - container.width) / 2);
1382
+ const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
1262
1383
 
1263
- media.style.transform = `scale(${scale}) translate(${constrainedX}px, ${constrainedY}px)`;
1384
+ // Constrain drag to overflow boundaries
1385
+ if (overflowX > 0) {
1386
+ newX = Math.max(-overflowX, Math.min(overflowX, newX));
1387
+ } else {
1388
+ newX = 0; // Center horizontally if no overflow
1389
+ }
1390
+
1391
+ if (overflowY > 0) {
1392
+ newY = Math.max(-overflowY, Math.min(overflowY, newY));
1393
+ } else {
1394
+ newY = 0; // Center vertically if no overflow
1395
+ }
1396
+
1397
+ // Apply transform
1398
+ media.style.transform = `translate(${newX}px, ${newY}px) scale(${this.currentScale})`;
1399
+
1400
+ // Update for next frame
1401
+ this.startX = clientX;
1402
+ this.startY = clientY;
1403
+ this.currentX = newX;
1404
+ this.currentY = newY;
1264
1405
  }
1265
1406
 
1266
1407
  /**
@@ -1270,9 +1411,14 @@ import './styles/zoomora.scss';
1270
1411
  if (!this.isDragging) return;
1271
1412
 
1272
1413
  this.isDragging = false;
1414
+ this.lastDragTime = Date.now();
1415
+
1273
1416
  const media = this.content.querySelector('.zoomora-media');
1274
- if (media && this.isZoomed) {
1275
- media.style.cursor = 'grab';
1417
+ if (media) {
1418
+ media.classList.remove('dragging');
1419
+ if (this.isZoomed) {
1420
+ media.style.cursor = 'grab';
1421
+ }
1276
1422
  }
1277
1423
  }
1278
1424