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/README.md +1 -1
- package/dist/zoomora.css +1 -1
- package/dist/zoomora.es.js +147 -42
- package/dist/zoomora.es.js.map +1 -1
- package/dist/zoomora.es.min.js +3 -3
- package/dist/zoomora.es.min.js.map +1 -1
- package/dist/zoomora.umd.js +147 -42
- package/dist/zoomora.umd.js.map +1 -1
- package/dist/zoomora.umd.min.js +3 -3
- package/dist/zoomora.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/src/styles/zoomora.scss +8 -1
- package/src/zoomora.js +197 -51
package/src/zoomora.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Zoomora Lightbox Plugin v1.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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')
|
|
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'
|
|
1148
|
+
if (!media || media.tagName !== 'IMG') return;
|
|
1075
1149
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1150
|
+
// Calculate zoom levels
|
|
1151
|
+
const zoomConfig = this.calculateZoomLevels(media);
|
|
1078
1152
|
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1153
|
+
if (!zoomConfig.canZoom) {
|
|
1154
|
+
console.log('Image cannot be zoomed (already at full size or larger)');
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1082
1157
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1158
|
+
// Get or initialize current zoom level
|
|
1159
|
+
if (!media.dataset.zoomLevel) {
|
|
1160
|
+
media.dataset.zoomLevel = '0';
|
|
1161
|
+
}
|
|
1086
1162
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
//
|
|
1224
|
-
const transform = media.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1261
|
-
const
|
|
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
|
-
|
|
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
|
|
1275
|
-
media.
|
|
1417
|
+
if (media) {
|
|
1418
|
+
media.classList.remove('dragging');
|
|
1419
|
+
if (this.isZoomed) {
|
|
1420
|
+
media.style.cursor = 'grab';
|
|
1421
|
+
}
|
|
1276
1422
|
}
|
|
1277
1423
|
}
|
|
1278
1424
|
|