zoomora 1.0.0 → 1.2.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 +75 -15
- package/dist/zoomora.css +1 -1
- package/dist/zoomora.es.js +232 -61
- package/dist/zoomora.es.js.map +1 -1
- package/dist/zoomora.es.min.js +4 -4
- package/dist/zoomora.es.min.js.map +1 -1
- package/dist/zoomora.umd.js +232 -61
- package/dist/zoomora.umd.js.map +1 -1
- package/dist/zoomora.umd.min.js +4 -4
- 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 +347 -88
package/src/zoomora.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Zoomora Lightbox Plugin v1.
|
|
2
|
+
* Zoomora Lightbox Plugin v1.2.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
|
|
|
@@ -46,6 +46,7 @@ import './styles/zoomora.scss';
|
|
|
46
46
|
maxZoomScale: 3, // Maximum zoom scale
|
|
47
47
|
zoomStep: 0.1, // Zoom step for scroll wheel
|
|
48
48
|
animationDuration: 300, // Animation duration in ms
|
|
49
|
+
closeOnBackgroundClick: true, // Close lightbox when clicking on background
|
|
49
50
|
onOpen: null, // Callback when lightbox opens
|
|
50
51
|
onClose: null, // Callback when lightbox closes
|
|
51
52
|
onNext: null, // Callback when navigating to next item
|
|
@@ -85,6 +86,8 @@ import './styles/zoomora.scss';
|
|
|
85
86
|
this.isControlsVisible = true;
|
|
86
87
|
this.idleTimer = null;
|
|
87
88
|
|
|
89
|
+
this.lastDragTime = 0;
|
|
90
|
+
|
|
88
91
|
// Bound methods for event listeners
|
|
89
92
|
this.boundMethods = {
|
|
90
93
|
handleDocumentClick: this.handleDocumentClick.bind(this),
|
|
@@ -147,6 +150,13 @@ import './styles/zoomora.scss';
|
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
// Build auto-hide button conditionally
|
|
154
|
+
const autoHideButtonHTML = this.options.showAutoHideToggle ? `
|
|
155
|
+
<button class="zoomora-btn" id="zoomoraAutoHideBtn" title="Toggle Auto-Hide Controls">
|
|
156
|
+
<i class="zoomorai-controls-off"></i>
|
|
157
|
+
</button>
|
|
158
|
+
` : '';
|
|
159
|
+
|
|
150
160
|
const zoomoraHTML = `
|
|
151
161
|
<div class="zoomora" id="zoomoraLightbox">
|
|
152
162
|
<div class="zoomora-container">
|
|
@@ -156,9 +166,7 @@ import './styles/zoomora.scss';
|
|
|
156
166
|
<button class="zoomora-btn" id="zoomoraThumbnailBtn" title="Toggle Thumbnails">
|
|
157
167
|
<i class="zoomorai-thumbnails"></i>
|
|
158
168
|
</button>
|
|
159
|
-
|
|
160
|
-
<i class="zoomorai-controls-off"></i>
|
|
161
|
-
</button>
|
|
169
|
+
${autoHideButtonHTML}
|
|
162
170
|
<button class="zoomora-btn" id="zoomoraZoomBtn" title="Zoom">
|
|
163
171
|
<i class="zoomorai-zoom"></i>
|
|
164
172
|
</button>
|
|
@@ -240,7 +248,7 @@ import './styles/zoomora.scss';
|
|
|
240
248
|
// Lightbox click to close
|
|
241
249
|
this.addEventListener(this.zoomora, 'click', this.boundMethods.handleZoomoraClick);
|
|
242
250
|
|
|
243
|
-
// Content click for zoom toggle
|
|
251
|
+
// Content click for zoom toggle and background click
|
|
244
252
|
this.addEventListener(this.content, 'click', this.boundMethods.handleContentClick);
|
|
245
253
|
|
|
246
254
|
// Mouse drag events
|
|
@@ -254,13 +262,15 @@ import './styles/zoomora.scss';
|
|
|
254
262
|
this.addEventListener(this.content, 'touchmove', this.boundMethods.handleTouchMove, {passive: false});
|
|
255
263
|
this.addEventListener(this.content, 'touchend', this.boundMethods.handleTouchEnd);
|
|
256
264
|
|
|
257
|
-
// Add auto-hide toggle button handler
|
|
258
|
-
this.
|
|
265
|
+
// Add auto-hide toggle button handler (only if enabled)
|
|
266
|
+
if (this.options.showAutoHideToggle) {
|
|
267
|
+
this.addEventListener(document.getElementById('zoomoraAutoHideBtn'), 'click', () => this.toggleAutoHide());
|
|
259
268
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
// Add mouse/touch activity listeners for auto-hide
|
|
270
|
+
this.addEventListener(this.zoomora, 'mousemove', () => this.handleUserActivity());
|
|
271
|
+
this.addEventListener(this.zoomora, 'touchstart', () => this.handleUserActivity());
|
|
272
|
+
this.addEventListener(this.zoomora, 'keydown', () => this.handleUserActivity());
|
|
273
|
+
}
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
/**
|
|
@@ -335,16 +345,25 @@ import './styles/zoomora.scss';
|
|
|
335
345
|
* Handle fullscreen change events
|
|
336
346
|
*/
|
|
337
347
|
handleFullscreenChange() {
|
|
338
|
-
const isInFullscreen =
|
|
348
|
+
const isInFullscreen = !!(
|
|
349
|
+
document.fullscreenElement ||
|
|
339
350
|
document.webkitFullscreenElement ||
|
|
340
351
|
document.mozFullScreenElement ||
|
|
341
|
-
document.msFullscreenElement
|
|
352
|
+
document.msFullscreenElement
|
|
353
|
+
);
|
|
342
354
|
|
|
355
|
+
// Only update if state changed from fullscreen to not fullscreen
|
|
343
356
|
if (!isInFullscreen && this.isFullscreen) {
|
|
344
357
|
this.isFullscreen = false;
|
|
345
358
|
this.zoomora.classList.remove('fullscreen');
|
|
346
359
|
this.updateButtons();
|
|
347
360
|
}
|
|
361
|
+
// Update if entering fullscreen
|
|
362
|
+
else if (isInFullscreen && !this.isFullscreen) {
|
|
363
|
+
this.isFullscreen = true;
|
|
364
|
+
this.zoomora.classList.add('fullscreen');
|
|
365
|
+
this.updateButtons();
|
|
366
|
+
}
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
/**
|
|
@@ -352,7 +371,7 @@ import './styles/zoomora.scss';
|
|
|
352
371
|
* @param {Event} e - Click event
|
|
353
372
|
*/
|
|
354
373
|
handleZoomoraClick(e) {
|
|
355
|
-
if (e.target === this.zoomora) {
|
|
374
|
+
if (e.target === this.zoomora && this.options.closeOnBackgroundClick) {
|
|
356
375
|
this.close();
|
|
357
376
|
}
|
|
358
377
|
}
|
|
@@ -362,9 +381,65 @@ import './styles/zoomora.scss';
|
|
|
362
381
|
* @param {Event} e - Click event
|
|
363
382
|
*/
|
|
364
383
|
handleContentClick(e) {
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
384
|
+
// Ignore click if we just finished dragging (within 200ms)
|
|
385
|
+
if (Date.now() - this.lastDragTime < 200) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Don't close on header clicks
|
|
390
|
+
if (this.header && this.header.contains(e.target)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Don't close on button clicks
|
|
395
|
+
if (e.target.closest('button')) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check if click is directly on actual media element (img, video, iframe)
|
|
400
|
+
const mediaElement = e.target.closest('.zoomora-media, .zoomora-video, .zoomora-iframe');
|
|
401
|
+
|
|
402
|
+
if (mediaElement) {
|
|
403
|
+
// Only toggle zoom for images
|
|
404
|
+
if (mediaElement.classList.contains('zoomora-media') && !mediaElement.classList.contains('no-zoom')) {
|
|
405
|
+
this.toggleZoom();
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check if clicking inside video container but not on video itself
|
|
411
|
+
const videoContainer = e.target.closest('.zoomora-video-container');
|
|
412
|
+
if (videoContainer) {
|
|
413
|
+
// If clicking on the play button, don't close
|
|
414
|
+
if (e.target.closest('.zoomora-play-button')) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// If clicking on background area of video container, close
|
|
418
|
+
if (this.options.closeOnBackgroundClick) {
|
|
419
|
+
this.close();
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if clicking on background elements (including when zoomed)
|
|
425
|
+
// This checks the actual target element, not just closest()
|
|
426
|
+
const clickedElement = e.target;
|
|
427
|
+
const isBackgroundElement =
|
|
428
|
+
clickedElement.classList.contains('zoomora-content') ||
|
|
429
|
+
clickedElement.classList.contains('zoomora-slide-container') ||
|
|
430
|
+
clickedElement.classList.contains('zoomora-slide') ||
|
|
431
|
+
clickedElement.classList.contains('zoomora-slides-container') ||
|
|
432
|
+
clickedElement.id === 'zoomoraContent' ||
|
|
433
|
+
clickedElement.id === 'zoomoraSlidesContainer';
|
|
434
|
+
|
|
435
|
+
if (isBackgroundElement && this.options.closeOnBackgroundClick) {
|
|
436
|
+
this.close();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// If we reach here and no media was clicked, close
|
|
441
|
+
if (this.options.closeOnBackgroundClick) {
|
|
442
|
+
this.close();
|
|
368
443
|
}
|
|
369
444
|
}
|
|
370
445
|
|
|
@@ -668,16 +743,19 @@ import './styles/zoomora.scss';
|
|
|
668
743
|
return;
|
|
669
744
|
}
|
|
670
745
|
|
|
746
|
+
// Reset zoom state
|
|
747
|
+
this.isZoomed = false;
|
|
748
|
+
this.currentX = 0;
|
|
749
|
+
this.currentY = 0;
|
|
750
|
+
this.currentScale = 1;
|
|
751
|
+
|
|
671
752
|
// Show loading spinner
|
|
672
753
|
this.loading.style.display = 'block';
|
|
673
754
|
|
|
674
|
-
// Clear content but keep the loading reference intact
|
|
675
|
-
// Don't replace innerHTML, just clear the slides
|
|
676
755
|
if (this.slidesContainer) {
|
|
677
756
|
this.slidesContainer.innerHTML = '';
|
|
678
757
|
}
|
|
679
758
|
|
|
680
|
-
// Add slight delay for smooth transition
|
|
681
759
|
setTimeout(() => {
|
|
682
760
|
this.renderContent(src, type, item);
|
|
683
761
|
}, 100);
|
|
@@ -953,6 +1031,58 @@ import './styles/zoomora.scss';
|
|
|
953
1031
|
}
|
|
954
1032
|
}
|
|
955
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Calculate zoom levels for an image
|
|
1036
|
+
* @param {HTMLImageElement} img - Image element
|
|
1037
|
+
* @returns {Object} Zoom configuration
|
|
1038
|
+
*/
|
|
1039
|
+
calculateZoomLevels(img) {
|
|
1040
|
+
if (!img || !img.naturalWidth || !img.naturalHeight) {
|
|
1041
|
+
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Get CONTAINER size (not current displayed image size)
|
|
1045
|
+
const container = this.content.getBoundingClientRect();
|
|
1046
|
+
const containerWidth = container.width;
|
|
1047
|
+
const containerHeight = container.height;
|
|
1048
|
+
|
|
1049
|
+
const naturalWidth = img.naturalWidth;
|
|
1050
|
+
const naturalHeight = img.naturalHeight;
|
|
1051
|
+
|
|
1052
|
+
// Calculate what the ORIGINAL fit-to-screen scale would be
|
|
1053
|
+
const scaleToFitWidth = containerWidth / naturalWidth;
|
|
1054
|
+
const scaleToFitHeight = containerHeight / naturalHeight;
|
|
1055
|
+
const fitScale = Math.min(scaleToFitWidth, scaleToFitHeight);
|
|
1056
|
+
|
|
1057
|
+
// If image naturally fits or is smaller than container, can't zoom
|
|
1058
|
+
if (fitScale >= 0.95) {
|
|
1059
|
+
return {canZoom: false, levels: [], currentLevel: 0};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Calculate zoom levels based on FIT scale (not current scale)
|
|
1063
|
+
const levels = [1]; // Start with fit-to-screen
|
|
1064
|
+
|
|
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);
|
|
1069
|
+
|
|
1070
|
+
// Add actual size (100%) as final level
|
|
1071
|
+
const actualSizeScale = 1 / fitScale;
|
|
1072
|
+
if (actualSizeScale > levels[levels.length - 1]) {
|
|
1073
|
+
levels.push(actualSizeScale);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
canZoom: levels.length > 1,
|
|
1078
|
+
levels: levels,
|
|
1079
|
+
currentLevel: 0,
|
|
1080
|
+
baseScale: fitScale,
|
|
1081
|
+
naturalWidth: naturalWidth,
|
|
1082
|
+
naturalHeight: naturalHeight
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
956
1086
|
/**
|
|
957
1087
|
* Update counter display
|
|
958
1088
|
*/
|
|
@@ -1071,29 +1201,70 @@ import './styles/zoomora.scss';
|
|
|
1071
1201
|
*/
|
|
1072
1202
|
toggleZoom() {
|
|
1073
1203
|
const media = this.content.querySelector('.zoomora-media');
|
|
1074
|
-
if (!media || media.tagName !== 'IMG'
|
|
1204
|
+
if (!media || media.tagName !== 'IMG') return;
|
|
1075
1205
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1206
|
+
// Calculate zoom levels
|
|
1207
|
+
const zoomConfig = this.calculateZoomLevels(media);
|
|
1078
1208
|
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1209
|
+
if (!zoomConfig.canZoom) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1082
1212
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1213
|
+
// Get or initialize current zoom level
|
|
1214
|
+
if (!media.dataset.zoomLevel) {
|
|
1215
|
+
media.dataset.zoomLevel = '0';
|
|
1216
|
+
}
|
|
1086
1217
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1218
|
+
let currentLevel = parseInt(media.dataset.zoomLevel) || 0;
|
|
1219
|
+
|
|
1220
|
+
// Cycle to next level
|
|
1221
|
+
currentLevel++;
|
|
1222
|
+
if (currentLevel >= zoomConfig.levels.length) {
|
|
1223
|
+
// Reset to fit-to-screen
|
|
1224
|
+
currentLevel = 0;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const targetScale = zoomConfig.levels[currentLevel];
|
|
1228
|
+
media.dataset.zoomLevel = currentLevel.toString();
|
|
1229
|
+
|
|
1230
|
+
if (currentLevel === 0) {
|
|
1231
|
+
// Back to fit-to-screen
|
|
1232
|
+
this.isZoomed = false;
|
|
1233
|
+
this.isDragging = false;
|
|
1234
|
+
media.classList.remove('dragging');
|
|
1092
1235
|
media.style.transform = '';
|
|
1093
|
-
media.style.cursor = '';
|
|
1236
|
+
media.style.cursor = 'pointer';
|
|
1237
|
+
media.classList.remove('zoomed');
|
|
1094
1238
|
this.currentX = 0;
|
|
1095
1239
|
this.currentY = 0;
|
|
1096
1240
|
this.currentScale = 1;
|
|
1241
|
+
} else {
|
|
1242
|
+
// Zoom in - reset position to center
|
|
1243
|
+
this.isZoomed = true;
|
|
1244
|
+
this.currentScale = targetScale;
|
|
1245
|
+
this.currentX = 0; // Always reset to center on zoom change
|
|
1246
|
+
this.currentY = 0;
|
|
1247
|
+
|
|
1248
|
+
media.style.transform = `translate(0px, 0px) scale(${targetScale})`;
|
|
1249
|
+
media.style.cursor = 'grab';
|
|
1250
|
+
media.classList.add('zoomed');
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
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)`;
|
|
1097
1268
|
}
|
|
1098
1269
|
}
|
|
1099
1270
|
|
|
@@ -1113,21 +1284,50 @@ import './styles/zoomora.scss';
|
|
|
1113
1284
|
*/
|
|
1114
1285
|
enterFullscreen() {
|
|
1115
1286
|
const element = this.zoomora;
|
|
1116
|
-
const requestFullscreen = element.requestFullscreen ||
|
|
1117
|
-
element.webkitRequestFullscreen ||
|
|
1118
|
-
element.mozRequestFullScreen ||
|
|
1119
|
-
element.msRequestFullscreen;
|
|
1120
1287
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1288
|
+
try {
|
|
1289
|
+
// Try standard API first
|
|
1290
|
+
if (element.requestFullscreen) {
|
|
1291
|
+
element.requestFullscreen()
|
|
1292
|
+
.then(() => {
|
|
1293
|
+
this.isFullscreen = true;
|
|
1294
|
+
this.zoomora.classList.add('fullscreen');
|
|
1295
|
+
this.updateButtons();
|
|
1296
|
+
})
|
|
1297
|
+
.catch(err => {
|
|
1298
|
+
console.warn('Zoomora: Fullscreen request failed:', err.message);
|
|
1299
|
+
this.fallbackFullscreen();
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
// Try webkit (Safari, older Chrome)
|
|
1303
|
+
else if (element.webkitRequestFullscreen) {
|
|
1304
|
+
element.webkitRequestFullscreen();
|
|
1305
|
+
// Webkit doesn't return a promise, so set state immediately
|
|
1306
|
+
this.isFullscreen = true;
|
|
1307
|
+
this.zoomora.classList.add('fullscreen');
|
|
1308
|
+
this.updateButtons();
|
|
1309
|
+
}
|
|
1310
|
+
// Try moz (Firefox)
|
|
1311
|
+
else if (element.mozRequestFullScreen) {
|
|
1312
|
+
element.mozRequestFullScreen();
|
|
1123
1313
|
this.isFullscreen = true;
|
|
1124
1314
|
this.zoomora.classList.add('fullscreen');
|
|
1125
1315
|
this.updateButtons();
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1316
|
+
}
|
|
1317
|
+
// Try ms (IE11)
|
|
1318
|
+
else if (element.msRequestFullscreen) {
|
|
1319
|
+
element.msRequestFullscreen();
|
|
1320
|
+
this.isFullscreen = true;
|
|
1321
|
+
this.zoomora.classList.add('fullscreen');
|
|
1322
|
+
this.updateButtons();
|
|
1323
|
+
}
|
|
1324
|
+
// Fallback to CSS fullscreen
|
|
1325
|
+
else {
|
|
1326
|
+
console.warn('Zoomora: Fullscreen API not supported, using CSS fallback');
|
|
1128
1327
|
this.fallbackFullscreen();
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1328
|
+
}
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
console.error('Zoomora: Error entering fullscreen:', error);
|
|
1131
1331
|
this.fallbackFullscreen();
|
|
1132
1332
|
}
|
|
1133
1333
|
}
|
|
@@ -1136,30 +1336,61 @@ import './styles/zoomora.scss';
|
|
|
1136
1336
|
* Exit fullscreen mode
|
|
1137
1337
|
*/
|
|
1138
1338
|
exitFullscreen() {
|
|
1139
|
-
|
|
1339
|
+
// First check if we're actually in fullscreen
|
|
1340
|
+
const isInFullscreen = !!(
|
|
1341
|
+
document.fullscreenElement ||
|
|
1140
1342
|
document.webkitFullscreenElement ||
|
|
1141
1343
|
document.mozFullScreenElement ||
|
|
1142
|
-
document.msFullscreenElement
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1344
|
+
document.msFullscreenElement
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
if (!isInFullscreen) {
|
|
1348
|
+
// Not in real fullscreen, just exit CSS fullscreen
|
|
1349
|
+
this.fallbackExitFullscreen();
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
try {
|
|
1354
|
+
// Try standard API first
|
|
1355
|
+
if (document.exitFullscreen) {
|
|
1356
|
+
document.exitFullscreen()
|
|
1357
|
+
.then(() => {
|
|
1358
|
+
this.isFullscreen = false;
|
|
1359
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1360
|
+
this.updateButtons();
|
|
1361
|
+
})
|
|
1362
|
+
.catch(err => {
|
|
1363
|
+
console.warn('Zoomora: Exit fullscreen failed:', err.message);
|
|
1364
|
+
this.fallbackExitFullscreen();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
// Try webkit
|
|
1368
|
+
else if (document.webkitExitFullscreen) {
|
|
1369
|
+
document.webkitExitFullscreen();
|
|
1370
|
+
this.isFullscreen = false;
|
|
1371
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1372
|
+
this.updateButtons();
|
|
1373
|
+
}
|
|
1374
|
+
// Try moz
|
|
1375
|
+
else if (document.mozCancelFullScreen) {
|
|
1376
|
+
document.mozCancelFullScreen();
|
|
1377
|
+
this.isFullscreen = false;
|
|
1378
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1379
|
+
this.updateButtons();
|
|
1380
|
+
}
|
|
1381
|
+
// Try ms
|
|
1382
|
+
else if (document.msExitFullscreen) {
|
|
1383
|
+
document.msExitFullscreen();
|
|
1384
|
+
this.isFullscreen = false;
|
|
1385
|
+
this.zoomora.classList.remove('fullscreen');
|
|
1386
|
+
this.updateButtons();
|
|
1387
|
+
}
|
|
1388
|
+
// Fallback
|
|
1389
|
+
else {
|
|
1160
1390
|
this.fallbackExitFullscreen();
|
|
1161
1391
|
}
|
|
1162
|
-
}
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
console.error('Zoomora: Error exiting fullscreen:', error);
|
|
1163
1394
|
this.fallbackExitFullscreen();
|
|
1164
1395
|
}
|
|
1165
1396
|
}
|
|
@@ -1215,19 +1446,22 @@ import './styles/zoomora.scss';
|
|
|
1215
1446
|
if (!media || !this.isZoomed) return;
|
|
1216
1447
|
|
|
1217
1448
|
this.isDragging = true;
|
|
1449
|
+
media.classList.add('dragging');
|
|
1218
1450
|
media.style.cursor = 'grabbing';
|
|
1219
1451
|
|
|
1220
1452
|
this.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
1221
1453
|
this.startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
1222
1454
|
|
|
1223
|
-
//
|
|
1224
|
-
const transform = media.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1455
|
+
// Read current position from transform
|
|
1456
|
+
const transform = window.getComputedStyle(media).transform;
|
|
1457
|
+
if (transform && transform !== 'none') {
|
|
1458
|
+
const matrix = new DOMMatrix(transform);
|
|
1459
|
+
this.currentX = matrix.m41;
|
|
1460
|
+
this.currentY = matrix.m42;
|
|
1461
|
+
} else {
|
|
1462
|
+
this.currentX = 0;
|
|
1463
|
+
this.currentY = 0;
|
|
1464
|
+
}
|
|
1231
1465
|
|
|
1232
1466
|
e.preventDefault();
|
|
1233
1467
|
}
|
|
@@ -1238,29 +1472,49 @@ import './styles/zoomora.scss';
|
|
|
1238
1472
|
*/
|
|
1239
1473
|
drag(e) {
|
|
1240
1474
|
if (!this.isDragging) return;
|
|
1241
|
-
|
|
1242
1475
|
e.preventDefault();
|
|
1476
|
+
|
|
1477
|
+
const media = this.content.querySelector('.zoomora-media');
|
|
1478
|
+
if (!media) return;
|
|
1479
|
+
|
|
1243
1480
|
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
1244
1481
|
const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
1245
1482
|
|
|
1246
1483
|
const deltaX = clientX - this.startX;
|
|
1247
1484
|
const deltaY = clientY - this.startY;
|
|
1248
1485
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const media = this.content.querySelector('.zoomora-media');
|
|
1253
|
-
if (!media) return;
|
|
1486
|
+
let newX = this.currentX + deltaX;
|
|
1487
|
+
let newY = this.currentY + deltaY;
|
|
1254
1488
|
|
|
1489
|
+
// Calculate boundaries for BOTH width AND height
|
|
1490
|
+
const container = this.content.getBoundingClientRect();
|
|
1255
1491
|
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
1492
|
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1493
|
+
// How much the scaled image overflows the container
|
|
1494
|
+
const overflowX = Math.max(0, (mediaRect.width - container.width) / 2);
|
|
1495
|
+
const overflowY = Math.max(0, (mediaRect.height - container.height) / 2);
|
|
1496
|
+
|
|
1497
|
+
// Constrain drag to overflow boundaries
|
|
1498
|
+
if (overflowX > 0) {
|
|
1499
|
+
newX = Math.max(-overflowX, Math.min(overflowX, newX));
|
|
1500
|
+
} else {
|
|
1501
|
+
newX = 0; // Center horizontally if no overflow
|
|
1502
|
+
}
|
|
1262
1503
|
|
|
1263
|
-
|
|
1504
|
+
if (overflowY > 0) {
|
|
1505
|
+
newY = Math.max(-overflowY, Math.min(overflowY, newY));
|
|
1506
|
+
} else {
|
|
1507
|
+
newY = 0; // Center vertically if no overflow
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Apply transform
|
|
1511
|
+
media.style.transform = `translate(${newX}px, ${newY}px) scale(${this.currentScale})`;
|
|
1512
|
+
|
|
1513
|
+
// Update for next frame
|
|
1514
|
+
this.startX = clientX;
|
|
1515
|
+
this.startY = clientY;
|
|
1516
|
+
this.currentX = newX;
|
|
1517
|
+
this.currentY = newY;
|
|
1264
1518
|
}
|
|
1265
1519
|
|
|
1266
1520
|
/**
|
|
@@ -1270,9 +1524,14 @@ import './styles/zoomora.scss';
|
|
|
1270
1524
|
if (!this.isDragging) return;
|
|
1271
1525
|
|
|
1272
1526
|
this.isDragging = false;
|
|
1527
|
+
this.lastDragTime = Date.now();
|
|
1528
|
+
|
|
1273
1529
|
const media = this.content.querySelector('.zoomora-media');
|
|
1274
|
-
if (media
|
|
1275
|
-
media.
|
|
1530
|
+
if (media) {
|
|
1531
|
+
media.classList.remove('dragging');
|
|
1532
|
+
if (this.isZoomed) {
|
|
1533
|
+
media.style.cursor = 'grab';
|
|
1534
|
+
}
|
|
1276
1535
|
}
|
|
1277
1536
|
}
|
|
1278
1537
|
|