@jekrch/react-viewport-lightbox 0.3.1 → 0.4.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/dist/index.cjs +411 -279
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +411 -279
- package/dist/index.js.map +1 -1
- package/dist/styles.css +23 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,6 +28,29 @@ function zoomToPoint(prevScale, nextScale, prev, focal, viewport) {
|
|
|
28
28
|
y: relY * (1 - k) + k * prev.y
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
function computeZoomTransform({
|
|
32
|
+
prevScale,
|
|
33
|
+
nextScale,
|
|
34
|
+
prev,
|
|
35
|
+
focal,
|
|
36
|
+
viewport,
|
|
37
|
+
baseDims,
|
|
38
|
+
zoomToCursor,
|
|
39
|
+
focalPan
|
|
40
|
+
}) {
|
|
41
|
+
if (nextScale <= 1) return { x: 0, y: 0 };
|
|
42
|
+
if (zoomToCursor) {
|
|
43
|
+
const f = zoomToPoint(prevScale, nextScale, prev, focal, viewport);
|
|
44
|
+
return clampTranslate(
|
|
45
|
+
f.x + (focalPan?.x ?? 0),
|
|
46
|
+
f.y + (focalPan?.y ?? 0),
|
|
47
|
+
nextScale,
|
|
48
|
+
baseDims,
|
|
49
|
+
viewport
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return clampTranslate(prev.x, prev.y, nextScale, baseDims, viewport);
|
|
53
|
+
}
|
|
31
54
|
function resolveSlideDirection({
|
|
32
55
|
offset,
|
|
33
56
|
elapsedMs,
|
|
@@ -48,6 +71,16 @@ function resolveSlideDirection({
|
|
|
48
71
|
// src/hooks/useImageZoomPan.ts
|
|
49
72
|
var MIN_SCALE = 1;
|
|
50
73
|
var MAX_SCALE = 5;
|
|
74
|
+
var DOUBLE_CLICK_ZOOM_SCALE = 1.8;
|
|
75
|
+
function resetWrapperStyles(wrapper) {
|
|
76
|
+
wrapper.style.transform = "none";
|
|
77
|
+
wrapper.style.position = "";
|
|
78
|
+
wrapper.style.inset = "";
|
|
79
|
+
wrapper.style.zIndex = "";
|
|
80
|
+
wrapper.style.backgroundColor = "";
|
|
81
|
+
wrapper.style.cursor = "";
|
|
82
|
+
wrapper.style.willChange = "";
|
|
83
|
+
}
|
|
51
84
|
function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCursor = true) {
|
|
52
85
|
const imgRef = useRef(null);
|
|
53
86
|
const [displayScale, setDisplayScale] = useState(1);
|
|
@@ -57,14 +90,9 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
57
90
|
(t, animate = false) => {
|
|
58
91
|
const wrapper = imgWrapperRef.current;
|
|
59
92
|
if (!wrapper) return;
|
|
60
|
-
wrapper.style.transition = animate ? "transform 0.2s ease-out" : "none";
|
|
93
|
+
wrapper.style.transition = typeof animate === "string" ? animate : animate ? "transform 0.2s ease-out" : "none";
|
|
61
94
|
if (t.scale <= 1) {
|
|
62
|
-
wrapper
|
|
63
|
-
wrapper.style.position = "";
|
|
64
|
-
wrapper.style.inset = "";
|
|
65
|
-
wrapper.style.zIndex = "";
|
|
66
|
-
wrapper.style.backgroundColor = "";
|
|
67
|
-
wrapper.style.cursor = "";
|
|
95
|
+
resetWrapperStyles(wrapper);
|
|
68
96
|
} else {
|
|
69
97
|
wrapper.style.transform = `scale(${t.scale}) translate(${t.x / t.scale}px, ${t.y / t.scale}px)`;
|
|
70
98
|
wrapper.style.position = "absolute";
|
|
@@ -72,6 +100,7 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
72
100
|
wrapper.style.zIndex = "30";
|
|
73
101
|
wrapper.style.backgroundColor = "black";
|
|
74
102
|
wrapper.style.cursor = "grab";
|
|
103
|
+
wrapper.style.willChange = "transform";
|
|
75
104
|
}
|
|
76
105
|
setDisplayScale(t.scale);
|
|
77
106
|
},
|
|
@@ -93,6 +122,9 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
93
122
|
if (!img) return;
|
|
94
123
|
baseDimsRef.current = { width: img.offsetWidth, height: img.offsetHeight };
|
|
95
124
|
}, []);
|
|
125
|
+
const ensureBaseDims = useCallback(() => {
|
|
126
|
+
if (baseDimsRef.current.width === 0) measureBaseDims();
|
|
127
|
+
}, [measureBaseDims]);
|
|
96
128
|
const clampTranslate2 = useCallback(
|
|
97
129
|
(x, y, scale) => clampTranslate(x, y, scale, baseDimsRef.current, {
|
|
98
130
|
width: window.innerWidth,
|
|
@@ -104,12 +136,7 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
104
136
|
const wrapper = imgWrapperRef.current;
|
|
105
137
|
if (wrapper) {
|
|
106
138
|
wrapper.style.transition = "none";
|
|
107
|
-
wrapper
|
|
108
|
-
wrapper.style.position = "";
|
|
109
|
-
wrapper.style.inset = "";
|
|
110
|
-
wrapper.style.zIndex = "";
|
|
111
|
-
wrapper.style.backgroundColor = "";
|
|
112
|
-
wrapper.style.cursor = "";
|
|
139
|
+
resetWrapperStyles(wrapper);
|
|
113
140
|
}
|
|
114
141
|
transformRef.current = { scale: 1, x: 0, y: 0 };
|
|
115
142
|
}, [currentIndex, imgWrapperRef]);
|
|
@@ -122,10 +149,7 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
122
149
|
if (!wrapper) return;
|
|
123
150
|
const handleWheel = (e) => {
|
|
124
151
|
e.preventDefault();
|
|
125
|
-
|
|
126
|
-
const img = imgRef.current;
|
|
127
|
-
if (img) baseDimsRef.current = { width: img.offsetWidth, height: img.offsetHeight };
|
|
128
|
-
}
|
|
152
|
+
ensureBaseDims();
|
|
129
153
|
const t = transformRef.current;
|
|
130
154
|
let dy = e.deltaY;
|
|
131
155
|
if (e.deltaMode === 1) dy *= 16;
|
|
@@ -134,41 +158,32 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
134
158
|
const step = -(normalized / 100) * 0.05;
|
|
135
159
|
const factor = 1 + step;
|
|
136
160
|
const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, t.scale * factor));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
148
|
-
clamped = clampTranslate2(focal.x, focal.y, nextScale);
|
|
149
|
-
} else {
|
|
150
|
-
clamped = clampTranslate2(t.x, t.y, nextScale);
|
|
151
|
-
}
|
|
152
|
-
setTransform({ scale: nextScale, ...clamped });
|
|
161
|
+
const clamped = computeZoomTransform({
|
|
162
|
+
prevScale: t.scale,
|
|
163
|
+
nextScale,
|
|
164
|
+
prev: { x: t.x, y: t.y },
|
|
165
|
+
focal: { x: e.clientX, y: e.clientY },
|
|
166
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
167
|
+
baseDims: baseDimsRef.current,
|
|
168
|
+
zoomToCursor
|
|
169
|
+
});
|
|
170
|
+
setTransform({ scale: nextScale, ...clamped }, "transform 0.1s ease-out");
|
|
153
171
|
};
|
|
154
172
|
wrapper.addEventListener("wheel", handleWheel, { passive: false });
|
|
155
173
|
return () => wrapper.removeEventListener("wheel", handleWheel);
|
|
156
|
-
}, [imgWrapperRef, setTransform,
|
|
174
|
+
}, [imgWrapperRef, setTransform, enabled, zoomToCursor, ensureBaseDims]);
|
|
157
175
|
const handleDoubleClick = useCallback(
|
|
158
176
|
(e) => {
|
|
159
177
|
if (!enabled) return;
|
|
160
178
|
e.stopPropagation();
|
|
161
|
-
|
|
162
|
-
const img = imgRef.current;
|
|
163
|
-
if (img) baseDimsRef.current = { width: img.offsetWidth, height: img.offsetHeight };
|
|
164
|
-
}
|
|
179
|
+
ensureBaseDims();
|
|
165
180
|
if (transformRef.current.scale > 1) {
|
|
166
181
|
resetTransform();
|
|
167
182
|
} else {
|
|
168
|
-
setTransform({ scale:
|
|
183
|
+
setTransform({ scale: DOUBLE_CLICK_ZOOM_SCALE, x: 0, y: 0 }, true);
|
|
169
184
|
}
|
|
170
185
|
},
|
|
171
|
-
[resetTransform, setTransform, enabled]
|
|
186
|
+
[resetTransform, setTransform, enabled, ensureBaseDims]
|
|
172
187
|
);
|
|
173
188
|
return {
|
|
174
189
|
imgRef,
|
|
@@ -184,13 +199,36 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCurs
|
|
|
184
199
|
handleDoubleClick
|
|
185
200
|
};
|
|
186
201
|
}
|
|
202
|
+
function measureSlideDistance(track) {
|
|
203
|
+
const vw = track?.clientWidth || window.innerWidth;
|
|
204
|
+
const img = track?.querySelector(".rvl-img-wrapper > img");
|
|
205
|
+
const imgW = img?.offsetWidth ?? 0;
|
|
206
|
+
if (!imgW) return vw;
|
|
207
|
+
return Math.min(vw, (vw + imgW) / 2);
|
|
208
|
+
}
|
|
209
|
+
function onTransitionEndOnce(el, cb, fallbackMs) {
|
|
210
|
+
if (!el) return;
|
|
211
|
+
let done = false;
|
|
212
|
+
const run = () => {
|
|
213
|
+
if (done) return;
|
|
214
|
+
done = true;
|
|
215
|
+
el.removeEventListener("transitionend", run);
|
|
216
|
+
cb();
|
|
217
|
+
};
|
|
218
|
+
el.addEventListener("transitionend", run, { once: true });
|
|
219
|
+
setTimeout(run, fallbackMs);
|
|
220
|
+
}
|
|
187
221
|
function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop = false) {
|
|
188
222
|
const slideTrackRef = useRef(null);
|
|
189
223
|
const swipeOffsetRef = useRef(0);
|
|
190
224
|
const [swipeOffset, setSwipeOffset] = useState(0);
|
|
191
225
|
const [slideAnimating, setSlideAnimating] = useState(false);
|
|
192
226
|
const [slideActive, setSlideActive] = useState(false);
|
|
227
|
+
const [slideDistance, setSlideDistance] = useState(0);
|
|
193
228
|
const commitLockRef = useRef(false);
|
|
229
|
+
const refreshSlideDistance = useCallback(() => {
|
|
230
|
+
setSlideDistance(measureSlideDistance(slideTrackRef.current));
|
|
231
|
+
}, []);
|
|
194
232
|
const hasPrev = loop ? items.length > 1 : currentIndex > 0;
|
|
195
233
|
const hasNext = loop ? items.length > 1 : currentIndex < items.length - 1;
|
|
196
234
|
const applySlideOffset = useCallback((offset, animate = false) => {
|
|
@@ -205,19 +243,14 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop
|
|
|
205
243
|
const snapBack = useCallback(() => {
|
|
206
244
|
setSlideAnimating(true);
|
|
207
245
|
applySlideOffset(0, true);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
if (track) {
|
|
218
|
-
track.addEventListener("transitionend", onEnd, { once: true });
|
|
219
|
-
setTimeout(onEnd, 350);
|
|
220
|
-
}
|
|
246
|
+
onTransitionEndOnce(
|
|
247
|
+
slideTrackRef.current,
|
|
248
|
+
() => {
|
|
249
|
+
setSlideAnimating(false);
|
|
250
|
+
setSlideActive(false);
|
|
251
|
+
},
|
|
252
|
+
350
|
|
253
|
+
);
|
|
221
254
|
}, [applySlideOffset]);
|
|
222
255
|
const readyRef = useRef(true);
|
|
223
256
|
const commitSlide = useCallback(
|
|
@@ -225,43 +258,38 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop
|
|
|
225
258
|
if (commitLockRef.current || !readyRef.current) return;
|
|
226
259
|
commitLockRef.current = true;
|
|
227
260
|
readyRef.current = false;
|
|
228
|
-
const
|
|
229
|
-
|
|
261
|
+
const distance = measureSlideDistance(slideTrackRef.current);
|
|
262
|
+
setSlideDistance(distance);
|
|
263
|
+
const targetOffset = direction === "prev" ? distance : -distance;
|
|
230
264
|
setSlideActive(true);
|
|
231
265
|
setSlideAnimating(true);
|
|
232
266
|
onSlideStart?.(direction);
|
|
233
267
|
requestAnimationFrame(() => {
|
|
234
268
|
applySlideOffset(targetOffset, true);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
const onTransitionEnd = () => cleanup();
|
|
261
|
-
if (track) {
|
|
262
|
-
track.addEventListener("transitionend", onTransitionEnd, { once: true });
|
|
263
|
-
setTimeout(cleanup, 400);
|
|
264
|
-
}
|
|
269
|
+
onTransitionEndOnce(
|
|
270
|
+
slideTrackRef.current,
|
|
271
|
+
() => {
|
|
272
|
+
let newIndex = direction === "prev" ? currentIndex - 1 : currentIndex + 1;
|
|
273
|
+
if (loop) newIndex = (newIndex + items.length) % items.length;
|
|
274
|
+
if (newIndex < 0 || newIndex >= items.length) {
|
|
275
|
+
commitLockRef.current = false;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const newItem = items[newIndex];
|
|
279
|
+
const preload = new Image();
|
|
280
|
+
preload.src = newItem.src;
|
|
281
|
+
const doNavigate = () => onNavigate(newIndex);
|
|
282
|
+
const timeout = setTimeout(doNavigate, 300);
|
|
283
|
+
preload.decode().then(() => {
|
|
284
|
+
clearTimeout(timeout);
|
|
285
|
+
doNavigate();
|
|
286
|
+
}).catch(() => {
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
doNavigate();
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
400
|
|
292
|
+
);
|
|
265
293
|
});
|
|
266
294
|
},
|
|
267
295
|
[applySlideOffset, currentIndex, items, onNavigate, onSlideStart, loop]
|
|
@@ -290,11 +318,11 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop
|
|
|
290
318
|
}
|
|
291
319
|
swipeOffsetRef.current = 0;
|
|
292
320
|
commitLockRef.current = false;
|
|
293
|
-
}, [currentIndex]);
|
|
294
|
-
useEffect(() => {
|
|
295
321
|
setSwipeOffset(0);
|
|
296
322
|
setSlideAnimating(false);
|
|
297
323
|
setSlideActive(false);
|
|
324
|
+
}, [currentIndex]);
|
|
325
|
+
useEffect(() => {
|
|
298
326
|
readyRef.current = true;
|
|
299
327
|
}, [currentIndex]);
|
|
300
328
|
return {
|
|
@@ -304,16 +332,33 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop
|
|
|
304
332
|
swipeOffset,
|
|
305
333
|
swipeOffsetRef,
|
|
306
334
|
commitLockRef,
|
|
335
|
+
slideDistance,
|
|
307
336
|
applySlideOffset,
|
|
308
337
|
commitSlide,
|
|
309
338
|
snapBack,
|
|
310
339
|
resolveSlide,
|
|
311
|
-
setSlideActive
|
|
340
|
+
setSlideActive,
|
|
341
|
+
refreshSlideDistance
|
|
312
342
|
};
|
|
313
343
|
}
|
|
344
|
+
var DOUBLE_TAP_ZOOM_SCALE = 2.5;
|
|
314
345
|
function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true, zoomToCursor = true) {
|
|
315
|
-
const {
|
|
316
|
-
|
|
346
|
+
const {
|
|
347
|
+
transformRef,
|
|
348
|
+
baseDimsRef,
|
|
349
|
+
clampTranslate: clampTranslate2,
|
|
350
|
+
setTransform,
|
|
351
|
+
applyTransform,
|
|
352
|
+
resetTransform
|
|
353
|
+
} = zoomPan;
|
|
354
|
+
const {
|
|
355
|
+
applySlideOffset,
|
|
356
|
+
resolveSlide,
|
|
357
|
+
snapBack,
|
|
358
|
+
setSlideActive,
|
|
359
|
+
swipeOffsetRef,
|
|
360
|
+
refreshSlideDistance
|
|
361
|
+
} = slide;
|
|
317
362
|
const panRef = useRef({
|
|
318
363
|
isDragging: false,
|
|
319
364
|
pointerStart: { x: 0, y: 0 },
|
|
@@ -336,9 +381,11 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
336
381
|
x: 0,
|
|
337
382
|
y: 0
|
|
338
383
|
});
|
|
384
|
+
const gestureMovedRef = useRef(false);
|
|
339
385
|
const beginSlide = useCallback(
|
|
340
386
|
(x, y) => {
|
|
341
387
|
setSlideActive(true);
|
|
388
|
+
refreshSlideDistance();
|
|
342
389
|
const sg = slideRef.current;
|
|
343
390
|
sg.active = true;
|
|
344
391
|
sg.startX = x;
|
|
@@ -347,7 +394,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
347
394
|
sg.locked = false;
|
|
348
395
|
sg.rejected = false;
|
|
349
396
|
},
|
|
350
|
-
[setSlideActive]
|
|
397
|
+
[setSlideActive, refreshSlideDistance]
|
|
351
398
|
);
|
|
352
399
|
const updateSlide = useCallback(
|
|
353
400
|
(clientX, clientY, lockThreshold, angleBias) => {
|
|
@@ -359,6 +406,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
359
406
|
const absDx = Math.abs(dx);
|
|
360
407
|
const absDy = Math.abs(dy);
|
|
361
408
|
if (absDx < lockThreshold && absDy < lockThreshold) return;
|
|
409
|
+
gestureMovedRef.current = true;
|
|
362
410
|
if (absDy > absDx * angleBias) {
|
|
363
411
|
sg.rejected = true;
|
|
364
412
|
return;
|
|
@@ -390,6 +438,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
390
438
|
const handlePointerDown = useCallback(
|
|
391
439
|
(e) => {
|
|
392
440
|
if (e.pointerType === "touch") return;
|
|
441
|
+
gestureMovedRef.current = false;
|
|
393
442
|
if (transformRef.current.scale > 1) {
|
|
394
443
|
e.preventDefault();
|
|
395
444
|
const p = panRef.current;
|
|
@@ -409,6 +458,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
409
458
|
if (p.isDragging && transformRef.current.scale > 1) {
|
|
410
459
|
const dx = e.clientX - p.pointerStart.x;
|
|
411
460
|
const dy = e.clientY - p.pointerStart.y;
|
|
461
|
+
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) gestureMovedRef.current = true;
|
|
412
462
|
const t = transformRef.current;
|
|
413
463
|
const clamped = clampTranslate2(p.translateStart.x + dx, p.translateStart.y + dy, t.scale);
|
|
414
464
|
setTransform({ scale: t.scale, ...clamped });
|
|
@@ -430,6 +480,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
430
480
|
(e) => {
|
|
431
481
|
const p = panRef.current;
|
|
432
482
|
if (e.touches.length === 2 && zoomEnabled) {
|
|
483
|
+
gestureMovedRef.current = true;
|
|
433
484
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
434
485
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
435
486
|
p.pinchStartDist = Math.hypot(dx, dy);
|
|
@@ -444,6 +495,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
444
495
|
snapBack();
|
|
445
496
|
}
|
|
446
497
|
} else if (e.touches.length === 1) {
|
|
498
|
+
gestureMovedRef.current = false;
|
|
447
499
|
if (transformRef.current.scale > 1) {
|
|
448
500
|
p.lastTouchPos = {
|
|
449
501
|
x: e.touches[0].clientX,
|
|
@@ -464,25 +516,26 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
464
516
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
465
517
|
const dist = Math.hypot(dx, dy);
|
|
466
518
|
const ratio = dist / p.pinchStartDist;
|
|
467
|
-
const nextScale = Math.min(
|
|
519
|
+
const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, p.pinchStartScale * ratio));
|
|
468
520
|
const t = transformRef.current;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
521
|
+
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
522
|
+
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
523
|
+
const prevMid = p.pinchMidpoint;
|
|
524
|
+
const focalPan = {
|
|
525
|
+
x: prevMid ? midX - prevMid.x : 0,
|
|
526
|
+
y: prevMid ? midY - prevMid.y : 0
|
|
527
|
+
};
|
|
528
|
+
p.pinchMidpoint = { x: midX, y: midY };
|
|
529
|
+
const clamped = computeZoomTransform({
|
|
530
|
+
prevScale: t.scale,
|
|
531
|
+
nextScale,
|
|
532
|
+
prev: { x: t.x, y: t.y },
|
|
533
|
+
focal: { x: midX, y: midY },
|
|
534
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
535
|
+
baseDims: baseDimsRef.current,
|
|
536
|
+
zoomToCursor,
|
|
537
|
+
focalPan
|
|
538
|
+
});
|
|
486
539
|
const next = { scale: nextScale, ...clamped };
|
|
487
540
|
transformRef.current = next;
|
|
488
541
|
applyTransform(next);
|
|
@@ -490,6 +543,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
490
543
|
const touch = e.touches[0];
|
|
491
544
|
const dx = touch.clientX - p.lastTouchPos.x;
|
|
492
545
|
const dy = touch.clientY - p.lastTouchPos.y;
|
|
546
|
+
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) gestureMovedRef.current = true;
|
|
493
547
|
p.lastTouchPos = { x: touch.clientX, y: touch.clientY };
|
|
494
548
|
const t = transformRef.current;
|
|
495
549
|
const clamped = clampTranslate2(t.x + dx, t.y + dy, t.scale);
|
|
@@ -501,7 +555,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
501
555
|
updateSlide(touch.clientX, touch.clientY, 6, 0.8);
|
|
502
556
|
}
|
|
503
557
|
},
|
|
504
|
-
[transformRef, clampTranslate2, applyTransform, updateSlide, zoomToCursor]
|
|
558
|
+
[transformRef, baseDimsRef, clampTranslate2, applyTransform, updateSlide, zoomToCursor]
|
|
505
559
|
);
|
|
506
560
|
const handleTouchEnd = useCallback(
|
|
507
561
|
(e) => {
|
|
@@ -541,7 +595,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
541
595
|
if (transformRef.current.scale > 1) {
|
|
542
596
|
resetTransform();
|
|
543
597
|
} else {
|
|
544
|
-
setTransform({ scale:
|
|
598
|
+
setTransform({ scale: DOUBLE_TAP_ZOOM_SCALE, x: 0, y: 0 }, true);
|
|
545
599
|
}
|
|
546
600
|
} else {
|
|
547
601
|
lastTapRef.current = { time: now, x: touch.clientX, y: touch.clientY };
|
|
@@ -568,7 +622,8 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true,
|
|
|
568
622
|
handlePointerUp,
|
|
569
623
|
handleTouchStart,
|
|
570
624
|
handleTouchMove,
|
|
571
|
-
handleTouchEnd
|
|
625
|
+
handleTouchEnd,
|
|
626
|
+
gestureMovedRef
|
|
572
627
|
};
|
|
573
628
|
}
|
|
574
629
|
function useBarMeasure(topBarRef, bottomBarRef, measureKey) {
|
|
@@ -667,6 +722,143 @@ function useFocusTrap(containerRef, active) {
|
|
|
667
722
|
};
|
|
668
723
|
}, [containerRef, active]);
|
|
669
724
|
}
|
|
725
|
+
var ANIM_MS = 250;
|
|
726
|
+
var IMG_PADDING = 44;
|
|
727
|
+
var ZOOM_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
728
|
+
function prefersReducedMotion() {
|
|
729
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
730
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
731
|
+
}
|
|
732
|
+
function flipTransform(from, to) {
|
|
733
|
+
const sx = to.width / from.width;
|
|
734
|
+
const sy = to.height / from.height;
|
|
735
|
+
const dx = to.left - from.left;
|
|
736
|
+
const dy = to.top - from.top;
|
|
737
|
+
return `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
|
|
738
|
+
}
|
|
739
|
+
function canAnimate(el) {
|
|
740
|
+
return !!el && typeof el.animate === "function";
|
|
741
|
+
}
|
|
742
|
+
function isRectInViewport(rect) {
|
|
743
|
+
if (typeof window === "undefined") return true;
|
|
744
|
+
return rect.top < window.innerHeight && rect.top + rect.height > 0 && rect.left < window.innerWidth && rect.left + rect.width > 0;
|
|
745
|
+
}
|
|
746
|
+
function useSharedElementZoom({
|
|
747
|
+
getOriginRect,
|
|
748
|
+
index,
|
|
749
|
+
isZoomed,
|
|
750
|
+
imgRef,
|
|
751
|
+
imgWrapperRef,
|
|
752
|
+
bottomBarRef,
|
|
753
|
+
measureBaseDims
|
|
754
|
+
}) {
|
|
755
|
+
const zoomTransition = !!getOriginRect;
|
|
756
|
+
const reduceMotion = prefersReducedMotion();
|
|
757
|
+
const gateEntry = zoomTransition && !reduceMotion;
|
|
758
|
+
const [fullLoaded, setFullLoaded] = useState(false);
|
|
759
|
+
const [showSpinner, setShowSpinner] = useState(false);
|
|
760
|
+
const [collapsing, setCollapsing] = useState(false);
|
|
761
|
+
const entryStartedRef = useRef(false);
|
|
762
|
+
const entryCleanupRef = useRef(null);
|
|
763
|
+
const runZoomEntry = useCallback(() => {
|
|
764
|
+
if (entryStartedRef.current) return;
|
|
765
|
+
if (!getOriginRect || prefersReducedMotion()) return;
|
|
766
|
+
const img = imgRef.current;
|
|
767
|
+
const thumb = getOriginRect(index);
|
|
768
|
+
if (!thumb || !canAnimate(img)) return;
|
|
769
|
+
const bottomH = bottomBarRef.current?.offsetHeight ?? 0;
|
|
770
|
+
const lockedMaxHeight = `calc(100vh - ${bottomH + IMG_PADDING * 2}px)`;
|
|
771
|
+
img.style.maxHeight = lockedMaxHeight;
|
|
772
|
+
const imgRect = img.getBoundingClientRect();
|
|
773
|
+
if (imgRect.width === 0 || imgRect.height === 0) {
|
|
774
|
+
img.style.maxHeight = "";
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
entryStartedRef.current = true;
|
|
778
|
+
const startTransform = flipTransform(imgRect, thumb);
|
|
779
|
+
img.style.transformOrigin = "top left";
|
|
780
|
+
img.style.transform = startTransform;
|
|
781
|
+
const wrapper = imgWrapperRef.current;
|
|
782
|
+
if (wrapper) wrapper.style.overflow = "visible";
|
|
783
|
+
const anim = img.animate(
|
|
784
|
+
[
|
|
785
|
+
{ transformOrigin: "top left", transform: startTransform },
|
|
786
|
+
{ transformOrigin: "top left", transform: "none" }
|
|
787
|
+
],
|
|
788
|
+
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
789
|
+
);
|
|
790
|
+
const cleanup = () => {
|
|
791
|
+
img.style.transform = "";
|
|
792
|
+
img.style.transformOrigin = "";
|
|
793
|
+
if (wrapper) wrapper.style.overflow = "";
|
|
794
|
+
img.style.maxHeight = lockedMaxHeight;
|
|
795
|
+
anim.cancel();
|
|
796
|
+
entryCleanupRef.current = null;
|
|
797
|
+
};
|
|
798
|
+
entryCleanupRef.current = cleanup;
|
|
799
|
+
anim.onfinish = cleanup;
|
|
800
|
+
}, [getOriginRect, index, imgRef, imgWrapperRef, bottomBarRef]);
|
|
801
|
+
const onImageLoad = useCallback(() => {
|
|
802
|
+
measureBaseDims();
|
|
803
|
+
const img = imgRef.current;
|
|
804
|
+
if (img && typeof img.decode === "function") {
|
|
805
|
+
img.decode().then(
|
|
806
|
+
() => setFullLoaded(true),
|
|
807
|
+
() => setFullLoaded(true)
|
|
808
|
+
);
|
|
809
|
+
} else {
|
|
810
|
+
setFullLoaded(true);
|
|
811
|
+
}
|
|
812
|
+
}, [measureBaseDims, imgRef]);
|
|
813
|
+
const onImageError = useCallback(() => setFullLoaded(true), []);
|
|
814
|
+
useLayoutEffect(() => {
|
|
815
|
+
const img = imgRef.current;
|
|
816
|
+
if (img && img.complete && img.naturalWidth > 0) onImageLoad();
|
|
817
|
+
}, []);
|
|
818
|
+
useLayoutEffect(() => {
|
|
819
|
+
if (fullLoaded) runZoomEntry();
|
|
820
|
+
}, [fullLoaded]);
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
if (!gateEntry || fullLoaded) {
|
|
823
|
+
setShowSpinner(false);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const t = setTimeout(() => setShowSpinner(true), 500);
|
|
827
|
+
return () => clearTimeout(t);
|
|
828
|
+
}, [gateEntry, fullLoaded]);
|
|
829
|
+
const settleEntry = useCallback(() => {
|
|
830
|
+
entryCleanupRef.current?.();
|
|
831
|
+
}, []);
|
|
832
|
+
const playCollapse = useCallback(() => {
|
|
833
|
+
const reduce = prefersReducedMotion();
|
|
834
|
+
const origin = !reduce && !isZoomed ? getOriginRect?.(index) ?? null : null;
|
|
835
|
+
const thumb = origin && isRectInViewport(origin) ? origin : null;
|
|
836
|
+
const img = imgRef.current;
|
|
837
|
+
if (!thumb || !canAnimate(img)) return;
|
|
838
|
+
const imgRect = img.getBoundingClientRect();
|
|
839
|
+
const wrapper = imgWrapperRef.current;
|
|
840
|
+
if (wrapper) wrapper.style.overflow = "visible";
|
|
841
|
+
setCollapsing(true);
|
|
842
|
+
img.animate(
|
|
843
|
+
[
|
|
844
|
+
{ transformOrigin: "top left", transform: "none" },
|
|
845
|
+
{ transformOrigin: "top left", transform: flipTransform(imgRect, thumb) }
|
|
846
|
+
],
|
|
847
|
+
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
848
|
+
);
|
|
849
|
+
}, [getOriginRect, index, isZoomed, imgRef, imgWrapperRef]);
|
|
850
|
+
return {
|
|
851
|
+
gateEntry,
|
|
852
|
+
zoomTransition,
|
|
853
|
+
fullLoaded,
|
|
854
|
+
showSpinner,
|
|
855
|
+
collapsing,
|
|
856
|
+
onImageLoad,
|
|
857
|
+
onImageError,
|
|
858
|
+
settleEntry,
|
|
859
|
+
playCollapse
|
|
860
|
+
};
|
|
861
|
+
}
|
|
670
862
|
function base(props) {
|
|
671
863
|
return {
|
|
672
864
|
width: 18,
|
|
@@ -737,28 +929,29 @@ function NavButton({ direction, enabled, onClick, icon, className }) {
|
|
|
737
929
|
}
|
|
738
930
|
);
|
|
739
931
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
932
|
+
function ChromeButton({
|
|
933
|
+
onClick,
|
|
934
|
+
title,
|
|
935
|
+
ariaLabel,
|
|
936
|
+
className,
|
|
937
|
+
children
|
|
938
|
+
}) {
|
|
939
|
+
return /* @__PURE__ */ jsx(
|
|
940
|
+
"button",
|
|
941
|
+
{
|
|
942
|
+
type: "button",
|
|
943
|
+
className: cx("rvl-btn", className),
|
|
944
|
+
onClick: (e) => {
|
|
945
|
+
e.stopPropagation();
|
|
946
|
+
onClick();
|
|
947
|
+
},
|
|
948
|
+
title,
|
|
949
|
+
"aria-label": ariaLabel,
|
|
950
|
+
children
|
|
951
|
+
}
|
|
952
|
+
);
|
|
761
953
|
}
|
|
954
|
+
var GHOST_CLICK_MS = 700;
|
|
762
955
|
function ImageViewer({
|
|
763
956
|
items,
|
|
764
957
|
index,
|
|
@@ -790,7 +983,6 @@ function ImageViewer({
|
|
|
790
983
|
}) {
|
|
791
984
|
const [visible, setVisible] = useState(false);
|
|
792
985
|
const [closing, setClosing] = useState(false);
|
|
793
|
-
const [collapsing, setCollapsing] = useState(false);
|
|
794
986
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
795
987
|
const [contentShift, setContentShiftState] = useState({ transform: null, animate: true });
|
|
796
988
|
const openedAtRef = useRef(Date.now());
|
|
@@ -828,8 +1020,27 @@ function ImageViewer({
|
|
|
828
1020
|
handleDoubleClick
|
|
829
1021
|
} = zoomPan;
|
|
830
1022
|
const slide = useSlideNavigation(items, index, onIndexChange, onNavigate, loop);
|
|
831
|
-
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, commitSlide } = slide;
|
|
1023
|
+
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, slideDistance, commitSlide } = slide;
|
|
832
1024
|
const gestures = useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoom, zoomToCursor);
|
|
1025
|
+
const {
|
|
1026
|
+
gateEntry,
|
|
1027
|
+
zoomTransition,
|
|
1028
|
+
fullLoaded,
|
|
1029
|
+
showSpinner,
|
|
1030
|
+
collapsing,
|
|
1031
|
+
onImageLoad,
|
|
1032
|
+
onImageError,
|
|
1033
|
+
settleEntry,
|
|
1034
|
+
playCollapse
|
|
1035
|
+
} = useSharedElementZoom({
|
|
1036
|
+
getOriginRect,
|
|
1037
|
+
index,
|
|
1038
|
+
isZoomed,
|
|
1039
|
+
imgRef,
|
|
1040
|
+
imgWrapperRef,
|
|
1041
|
+
bottomBarRef,
|
|
1042
|
+
measureBaseDims
|
|
1043
|
+
});
|
|
833
1044
|
useEffect(() => {
|
|
834
1045
|
if (typeof window === "undefined" || !window.matchMedia) {
|
|
835
1046
|
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
@@ -850,101 +1061,14 @@ function ImageViewer({
|
|
|
850
1061
|
const raf = requestAnimationFrame(() => setVisible(true));
|
|
851
1062
|
return () => cancelAnimationFrame(raf);
|
|
852
1063
|
}, []);
|
|
853
|
-
const zoomTransition = !!getOriginRect;
|
|
854
|
-
const reduceMotion = prefersReducedMotion();
|
|
855
|
-
const gateEntry = zoomTransition && !reduceMotion;
|
|
856
|
-
const [fullLoaded, setFullLoaded] = useState(false);
|
|
857
|
-
const [showSpinner, setShowSpinner] = useState(false);
|
|
858
|
-
const entryStartedRef = useRef(false);
|
|
859
|
-
const entryCleanupRef = useRef(null);
|
|
860
|
-
const runZoomEntry = useCallback(() => {
|
|
861
|
-
if (entryStartedRef.current) return;
|
|
862
|
-
if (!getOriginRect || prefersReducedMotion()) return;
|
|
863
|
-
const img = imgRef.current;
|
|
864
|
-
const thumb = getOriginRect(index);
|
|
865
|
-
if (!thumb || !canAnimate(img)) return;
|
|
866
|
-
const bottomH = bottomBarRef.current?.offsetHeight ?? 0;
|
|
867
|
-
const lockedMaxHeight = `calc(100vh - ${bottomH + IMG_PADDING * 2}px)`;
|
|
868
|
-
img.style.maxHeight = lockedMaxHeight;
|
|
869
|
-
const imgRect = img.getBoundingClientRect();
|
|
870
|
-
if (imgRect.width === 0 || imgRect.height === 0) {
|
|
871
|
-
img.style.maxHeight = "";
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
entryStartedRef.current = true;
|
|
875
|
-
const startTransform = flipTransform(imgRect, thumb);
|
|
876
|
-
img.style.transformOrigin = "top left";
|
|
877
|
-
img.style.transform = startTransform;
|
|
878
|
-
const wrapper = imgWrapperRef.current;
|
|
879
|
-
if (wrapper) wrapper.style.overflow = "visible";
|
|
880
|
-
const anim = img.animate(
|
|
881
|
-
[
|
|
882
|
-
{ transformOrigin: "top left", transform: startTransform },
|
|
883
|
-
{ transformOrigin: "top left", transform: "none" }
|
|
884
|
-
],
|
|
885
|
-
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
886
|
-
);
|
|
887
|
-
const cleanup = () => {
|
|
888
|
-
img.style.transform = "";
|
|
889
|
-
img.style.transformOrigin = "";
|
|
890
|
-
if (wrapper) wrapper.style.overflow = "";
|
|
891
|
-
img.style.maxHeight = lockedMaxHeight;
|
|
892
|
-
anim.cancel();
|
|
893
|
-
entryCleanupRef.current = null;
|
|
894
|
-
};
|
|
895
|
-
entryCleanupRef.current = cleanup;
|
|
896
|
-
anim.onfinish = cleanup;
|
|
897
|
-
}, [getOriginRect, index, imgRef, imgWrapperRef, bottomBarRef]);
|
|
898
|
-
const markFullLoaded = useCallback(() => {
|
|
899
|
-
measureBaseDims();
|
|
900
|
-
const img = imgRef.current;
|
|
901
|
-
if (img && typeof img.decode === "function") {
|
|
902
|
-
img.decode().then(
|
|
903
|
-
() => setFullLoaded(true),
|
|
904
|
-
() => setFullLoaded(true)
|
|
905
|
-
);
|
|
906
|
-
} else {
|
|
907
|
-
setFullLoaded(true);
|
|
908
|
-
}
|
|
909
|
-
}, [measureBaseDims, imgRef]);
|
|
910
|
-
useLayoutEffect(() => {
|
|
911
|
-
const img = imgRef.current;
|
|
912
|
-
if (img && img.complete && img.naturalWidth > 0) markFullLoaded();
|
|
913
|
-
}, []);
|
|
914
|
-
useLayoutEffect(() => {
|
|
915
|
-
if (fullLoaded) runZoomEntry();
|
|
916
|
-
}, [fullLoaded]);
|
|
917
|
-
useEffect(() => {
|
|
918
|
-
if (!gateEntry || fullLoaded) {
|
|
919
|
-
setShowSpinner(false);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const t = setTimeout(() => setShowSpinner(true), 500);
|
|
923
|
-
return () => clearTimeout(t);
|
|
924
|
-
}, [gateEntry, fullLoaded]);
|
|
925
1064
|
const handleClose = useCallback(() => {
|
|
926
1065
|
const reduce = prefersReducedMotion();
|
|
927
|
-
|
|
928
|
-
const thumb = origin && isRectInViewport(origin) ? origin : null;
|
|
929
|
-
const img = imgRef.current;
|
|
930
|
-
entryCleanupRef.current?.();
|
|
1066
|
+
settleEntry();
|
|
931
1067
|
setClosing(true);
|
|
932
1068
|
setVisible(false);
|
|
933
|
-
|
|
934
|
-
const imgRect = img.getBoundingClientRect();
|
|
935
|
-
const wrapper = imgWrapperRef.current;
|
|
936
|
-
if (wrapper) wrapper.style.overflow = "visible";
|
|
937
|
-
setCollapsing(true);
|
|
938
|
-
img.animate(
|
|
939
|
-
[
|
|
940
|
-
{ transformOrigin: "top left", transform: "none" },
|
|
941
|
-
{ transformOrigin: "top left", transform: flipTransform(imgRect, thumb) }
|
|
942
|
-
],
|
|
943
|
-
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
944
|
-
);
|
|
945
|
-
}
|
|
1069
|
+
playCollapse();
|
|
946
1070
|
setTimeout(onClose, reduce ? 0 : ANIM_MS);
|
|
947
|
-
}, [onClose,
|
|
1071
|
+
}, [onClose, settleEntry, playCollapse]);
|
|
948
1072
|
const handleBackdropTouchEnd = useCallback(
|
|
949
1073
|
(e) => {
|
|
950
1074
|
if (e.target !== e.currentTarget) return;
|
|
@@ -953,6 +1077,26 @@ function ImageViewer({
|
|
|
953
1077
|
},
|
|
954
1078
|
[handleClose]
|
|
955
1079
|
);
|
|
1080
|
+
const handleTrackTouchEnd = useCallback(
|
|
1081
|
+
(e) => {
|
|
1082
|
+
gestures.handleTouchEnd(e);
|
|
1083
|
+
if (!closeOnBackdropClick) return;
|
|
1084
|
+
if (e.target !== e.currentTarget) return;
|
|
1085
|
+
if (gestures.gestureMovedRef.current) return;
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
handleClose();
|
|
1088
|
+
},
|
|
1089
|
+
[gestures, closeOnBackdropClick, handleClose]
|
|
1090
|
+
);
|
|
1091
|
+
const handleTrackClick = useCallback(
|
|
1092
|
+
(e) => {
|
|
1093
|
+
if (e.target !== e.currentTarget) return;
|
|
1094
|
+
if (isGhostMouseEvent()) return;
|
|
1095
|
+
if (gestures.gestureMovedRef.current) return;
|
|
1096
|
+
handleClose();
|
|
1097
|
+
},
|
|
1098
|
+
[gestures, isGhostMouseEvent, handleClose]
|
|
1099
|
+
);
|
|
956
1100
|
const handleDoubleClickGuarded = useCallback(
|
|
957
1101
|
(e) => {
|
|
958
1102
|
if (isGhostMouseEvent()) return;
|
|
@@ -964,14 +1108,6 @@ function ImageViewer({
|
|
|
964
1108
|
if (isGhostMouseEvent()) return;
|
|
965
1109
|
handleClose();
|
|
966
1110
|
}, [handleClose, isGhostMouseEvent]);
|
|
967
|
-
const handleStageClick = useCallback(
|
|
968
|
-
(e) => {
|
|
969
|
-
if (e.target !== e.currentTarget) return;
|
|
970
|
-
if (isGhostMouseEvent()) return;
|
|
971
|
-
handleClose();
|
|
972
|
-
},
|
|
973
|
-
[handleClose, isGhostMouseEvent]
|
|
974
|
-
);
|
|
975
1111
|
const navigate = useCallback(
|
|
976
1112
|
(dir) => {
|
|
977
1113
|
if (dir === "prev") {
|
|
@@ -1051,7 +1187,9 @@ function ImageViewer({
|
|
|
1051
1187
|
const prevItem = prevIndex >= 0 ? items[prevIndex] : null;
|
|
1052
1188
|
const nextItem = nextIndex >= 0 ? items[nextIndex] : null;
|
|
1053
1189
|
const showAdjacent = slideActive || slideAnimating || swipeOffset !== 0;
|
|
1054
|
-
const
|
|
1190
|
+
const adjacentOffset = slideDistance || viewportWidth;
|
|
1191
|
+
const adjacentOpacity = Math.min(1, Math.abs(swipeOffset) / (adjacentOffset * 0.8 || 1));
|
|
1192
|
+
const adjacentTransition = slideAnimating ? "opacity 0.28s cubic-bezier(0.2, 0, 0, 1)" : "none";
|
|
1055
1193
|
const showZoomCtrls = zoom && !isTouchDevice && showZoomControls && !contentShift.transform;
|
|
1056
1194
|
const headerActions = renderHeaderActions?.(ctx);
|
|
1057
1195
|
const navStart = renderNavStart?.(ctx);
|
|
@@ -1083,16 +1221,12 @@ function ImageViewer({
|
|
|
1083
1221
|
/* @__PURE__ */ jsxs("div", { className: "rvl-header-actions", children: [
|
|
1084
1222
|
headerActions,
|
|
1085
1223
|
showZoomCtrls && isZoomed && /* @__PURE__ */ jsxs(
|
|
1086
|
-
|
|
1224
|
+
ChromeButton,
|
|
1087
1225
|
{
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
onClick: (e) => {
|
|
1091
|
-
e.stopPropagation();
|
|
1092
|
-
resetTransform();
|
|
1093
|
-
},
|
|
1226
|
+
className: cx("rvl-btn-scale", cn("button")),
|
|
1227
|
+
onClick: resetTransform,
|
|
1094
1228
|
title: "Reset zoom",
|
|
1095
|
-
|
|
1229
|
+
ariaLabel: "Reset zoom",
|
|
1096
1230
|
children: [
|
|
1097
1231
|
Math.round(displayScale * 100),
|
|
1098
1232
|
"%"
|
|
@@ -1100,44 +1234,32 @@ function ImageViewer({
|
|
|
1100
1234
|
}
|
|
1101
1235
|
),
|
|
1102
1236
|
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1103
|
-
|
|
1237
|
+
ChromeButton,
|
|
1104
1238
|
{
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
onClick: (e) => {
|
|
1108
|
-
e.stopPropagation();
|
|
1109
|
-
ctx.zoomIn();
|
|
1110
|
-
},
|
|
1239
|
+
className: cn("button"),
|
|
1240
|
+
onClick: ctx.zoomIn,
|
|
1111
1241
|
title: "Zoom in",
|
|
1112
|
-
|
|
1242
|
+
ariaLabel: "Zoom in",
|
|
1113
1243
|
children: mergedIcons.zoomIn
|
|
1114
1244
|
}
|
|
1115
1245
|
),
|
|
1116
1246
|
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1117
|
-
|
|
1247
|
+
ChromeButton,
|
|
1118
1248
|
{
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
onClick: (e) => {
|
|
1122
|
-
e.stopPropagation();
|
|
1123
|
-
ctx.zoomOut();
|
|
1124
|
-
},
|
|
1249
|
+
className: cn("button"),
|
|
1250
|
+
onClick: ctx.zoomOut,
|
|
1125
1251
|
title: "Zoom out",
|
|
1126
|
-
|
|
1252
|
+
ariaLabel: "Zoom out",
|
|
1127
1253
|
children: mergedIcons.zoomOut
|
|
1128
1254
|
}
|
|
1129
1255
|
),
|
|
1130
1256
|
/* @__PURE__ */ jsx(
|
|
1131
|
-
|
|
1257
|
+
ChromeButton,
|
|
1132
1258
|
{
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
onClick: (e) => {
|
|
1136
|
-
e.stopPropagation();
|
|
1137
|
-
handleClose();
|
|
1138
|
-
},
|
|
1259
|
+
className: cn("button"),
|
|
1260
|
+
onClick: handleClose,
|
|
1139
1261
|
title: "Close (Esc)",
|
|
1140
|
-
|
|
1262
|
+
ariaLabel: "Close",
|
|
1141
1263
|
children: mergedIcons.close
|
|
1142
1264
|
}
|
|
1143
1265
|
)
|
|
@@ -1147,8 +1269,6 @@ function ImageViewer({
|
|
|
1147
1269
|
"div",
|
|
1148
1270
|
{
|
|
1149
1271
|
className: "rvl-stage",
|
|
1150
|
-
onClick: closeOnBackdropClick ? handleStageClick : void 0,
|
|
1151
|
-
onTouchEnd: closeOnBackdropClick ? handleBackdropTouchEnd : void 0,
|
|
1152
1272
|
style: {
|
|
1153
1273
|
transform: contentShift.transform ?? "translateY(0)",
|
|
1154
1274
|
// animate=false snaps with no transition (overrides the CSS transition)
|
|
@@ -1158,8 +1278,19 @@ function ImageViewer({
|
|
|
1158
1278
|
"div",
|
|
1159
1279
|
{
|
|
1160
1280
|
ref: slideTrackRef,
|
|
1281
|
+
onPointerDown: gestures.handlePointerDown,
|
|
1282
|
+
onPointerMove: gestures.handlePointerMove,
|
|
1283
|
+
onPointerUp: gestures.handlePointerUp,
|
|
1284
|
+
onPointerLeave: gestures.handlePointerUp,
|
|
1285
|
+
onTouchStart: gestures.handleTouchStart,
|
|
1286
|
+
onTouchMove: gestures.handleTouchMove,
|
|
1287
|
+
onTouchEnd: handleTrackTouchEnd,
|
|
1288
|
+
onClick: closeOnBackdropClick ? handleTrackClick : void 0,
|
|
1161
1289
|
className: cx(
|
|
1162
1290
|
"rvl-track",
|
|
1291
|
+
// Promote the track only while a swipe is live (drag + commit/snap
|
|
1292
|
+
// animation), then release the layer. Matches `showAdjacent`.
|
|
1293
|
+
showAdjacent && "rvl-track-swiping",
|
|
1163
1294
|
// During a thumbnail zoom the track is opaque from the first frame
|
|
1164
1295
|
// (the image itself is hidden until the zoom starts), so the picture
|
|
1165
1296
|
// flies in crisply instead of cross-fading. On close it only stays
|
|
@@ -1172,7 +1303,11 @@ function ImageViewer({
|
|
|
1172
1303
|
"div",
|
|
1173
1304
|
{
|
|
1174
1305
|
className: "rvl-adjacent",
|
|
1175
|
-
style: {
|
|
1306
|
+
style: {
|
|
1307
|
+
transform: `translateX(${-adjacentOffset}px)`,
|
|
1308
|
+
opacity: swipeOffset > 0 ? adjacentOpacity : 0,
|
|
1309
|
+
transition: adjacentTransition
|
|
1310
|
+
},
|
|
1176
1311
|
children: /* @__PURE__ */ jsx(
|
|
1177
1312
|
"img",
|
|
1178
1313
|
{
|
|
@@ -1192,13 +1327,6 @@ function ImageViewer({
|
|
|
1192
1327
|
className: "rvl-img-wrapper",
|
|
1193
1328
|
onClick: (e) => e.stopPropagation(),
|
|
1194
1329
|
onDoubleClick: handleDoubleClickGuarded,
|
|
1195
|
-
onPointerDown: gestures.handlePointerDown,
|
|
1196
|
-
onPointerMove: gestures.handlePointerMove,
|
|
1197
|
-
onPointerUp: gestures.handlePointerUp,
|
|
1198
|
-
onPointerLeave: gestures.handlePointerUp,
|
|
1199
|
-
onTouchStart: gestures.handleTouchStart,
|
|
1200
|
-
onTouchMove: gestures.handleTouchMove,
|
|
1201
|
-
onTouchEnd: gestures.handleTouchEnd,
|
|
1202
1330
|
children: /* @__PURE__ */ jsx(
|
|
1203
1331
|
"img",
|
|
1204
1332
|
{
|
|
@@ -1208,8 +1336,8 @@ function ImageViewer({
|
|
|
1208
1336
|
className: cx("rvl-img", cn("image")),
|
|
1209
1337
|
style: imgStyle,
|
|
1210
1338
|
draggable: false,
|
|
1211
|
-
onLoad:
|
|
1212
|
-
onError:
|
|
1339
|
+
onLoad: onImageLoad,
|
|
1340
|
+
onError: onImageError
|
|
1213
1341
|
}
|
|
1214
1342
|
)
|
|
1215
1343
|
}
|
|
@@ -1219,7 +1347,11 @@ function ImageViewer({
|
|
|
1219
1347
|
"div",
|
|
1220
1348
|
{
|
|
1221
1349
|
className: "rvl-adjacent",
|
|
1222
|
-
style: {
|
|
1350
|
+
style: {
|
|
1351
|
+
transform: `translateX(${adjacentOffset}px)`,
|
|
1352
|
+
opacity: swipeOffset < 0 ? adjacentOpacity : 0,
|
|
1353
|
+
transition: adjacentTransition
|
|
1354
|
+
},
|
|
1223
1355
|
children: /* @__PURE__ */ jsx(
|
|
1224
1356
|
"img",
|
|
1225
1357
|
{
|