@jekrch/react-viewport-lightbox 0.3.0 → 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 +438 -273
- 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 +438 -273
- package/dist/index.js.map +1 -1
- package/dist/styles.css +27 -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,27 +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
|
-
|
|
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
|
+
);
|
|
760
953
|
}
|
|
954
|
+
var GHOST_CLICK_MS = 700;
|
|
761
955
|
function ImageViewer({
|
|
762
956
|
items,
|
|
763
957
|
index,
|
|
@@ -789,9 +983,13 @@ function ImageViewer({
|
|
|
789
983
|
}) {
|
|
790
984
|
const [visible, setVisible] = useState(false);
|
|
791
985
|
const [closing, setClosing] = useState(false);
|
|
792
|
-
const [collapsing, setCollapsing] = useState(false);
|
|
793
986
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
794
987
|
const [contentShift, setContentShiftState] = useState({ transform: null, animate: true });
|
|
988
|
+
const openedAtRef = useRef(Date.now());
|
|
989
|
+
const isGhostMouseEvent = useCallback(
|
|
990
|
+
() => Date.now() - openedAtRef.current < GHOST_CLICK_MS,
|
|
991
|
+
[]
|
|
992
|
+
);
|
|
795
993
|
const containerRef = useRef(null);
|
|
796
994
|
const imgWrapperRef = useRef(null);
|
|
797
995
|
const topBarRef = useRef(null);
|
|
@@ -822,8 +1020,27 @@ function ImageViewer({
|
|
|
822
1020
|
handleDoubleClick
|
|
823
1021
|
} = zoomPan;
|
|
824
1022
|
const slide = useSlideNavigation(items, index, onIndexChange, onNavigate, loop);
|
|
825
|
-
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, commitSlide } = slide;
|
|
1023
|
+
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, slideDistance, commitSlide } = slide;
|
|
826
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
|
+
});
|
|
827
1044
|
useEffect(() => {
|
|
828
1045
|
if (typeof window === "undefined" || !window.matchMedia) {
|
|
829
1046
|
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
@@ -844,101 +1061,53 @@ function ImageViewer({
|
|
|
844
1061
|
const raf = requestAnimationFrame(() => setVisible(true));
|
|
845
1062
|
return () => cancelAnimationFrame(raf);
|
|
846
1063
|
}, []);
|
|
847
|
-
const zoomTransition = !!getOriginRect;
|
|
848
|
-
const reduceMotion = prefersReducedMotion();
|
|
849
|
-
const gateEntry = zoomTransition && !reduceMotion;
|
|
850
|
-
const [fullLoaded, setFullLoaded] = useState(false);
|
|
851
|
-
const [showSpinner, setShowSpinner] = useState(false);
|
|
852
|
-
const entryStartedRef = useRef(false);
|
|
853
|
-
const entryCleanupRef = useRef(null);
|
|
854
|
-
const runZoomEntry = useCallback(() => {
|
|
855
|
-
if (entryStartedRef.current) return;
|
|
856
|
-
if (!getOriginRect || prefersReducedMotion()) return;
|
|
857
|
-
const img = imgRef.current;
|
|
858
|
-
const thumb = getOriginRect(index);
|
|
859
|
-
if (!thumb || !canAnimate(img)) return;
|
|
860
|
-
const bottomH = bottomBarRef.current?.offsetHeight ?? 0;
|
|
861
|
-
const lockedMaxHeight = `calc(100vh - ${bottomH + IMG_PADDING * 2}px)`;
|
|
862
|
-
img.style.maxHeight = lockedMaxHeight;
|
|
863
|
-
const imgRect = img.getBoundingClientRect();
|
|
864
|
-
if (imgRect.width === 0 || imgRect.height === 0) {
|
|
865
|
-
img.style.maxHeight = "";
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
entryStartedRef.current = true;
|
|
869
|
-
const startTransform = flipTransform(imgRect, thumb);
|
|
870
|
-
img.style.transformOrigin = "top left";
|
|
871
|
-
img.style.transform = startTransform;
|
|
872
|
-
const wrapper = imgWrapperRef.current;
|
|
873
|
-
if (wrapper) wrapper.style.overflow = "visible";
|
|
874
|
-
const anim = img.animate(
|
|
875
|
-
[
|
|
876
|
-
{ transformOrigin: "top left", transform: startTransform },
|
|
877
|
-
{ transformOrigin: "top left", transform: "none" }
|
|
878
|
-
],
|
|
879
|
-
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
880
|
-
);
|
|
881
|
-
const cleanup = () => {
|
|
882
|
-
img.style.transform = "";
|
|
883
|
-
img.style.transformOrigin = "";
|
|
884
|
-
if (wrapper) wrapper.style.overflow = "";
|
|
885
|
-
img.style.maxHeight = lockedMaxHeight;
|
|
886
|
-
anim.cancel();
|
|
887
|
-
entryCleanupRef.current = null;
|
|
888
|
-
};
|
|
889
|
-
entryCleanupRef.current = cleanup;
|
|
890
|
-
anim.onfinish = cleanup;
|
|
891
|
-
}, [getOriginRect, index, imgRef, imgWrapperRef, bottomBarRef]);
|
|
892
|
-
const markFullLoaded = useCallback(() => {
|
|
893
|
-
measureBaseDims();
|
|
894
|
-
const img = imgRef.current;
|
|
895
|
-
if (img && typeof img.decode === "function") {
|
|
896
|
-
img.decode().then(
|
|
897
|
-
() => setFullLoaded(true),
|
|
898
|
-
() => setFullLoaded(true)
|
|
899
|
-
);
|
|
900
|
-
} else {
|
|
901
|
-
setFullLoaded(true);
|
|
902
|
-
}
|
|
903
|
-
}, [measureBaseDims, imgRef]);
|
|
904
|
-
useLayoutEffect(() => {
|
|
905
|
-
const img = imgRef.current;
|
|
906
|
-
if (img && img.complete && img.naturalWidth > 0) markFullLoaded();
|
|
907
|
-
}, []);
|
|
908
|
-
useLayoutEffect(() => {
|
|
909
|
-
if (fullLoaded) runZoomEntry();
|
|
910
|
-
}, [fullLoaded]);
|
|
911
|
-
useEffect(() => {
|
|
912
|
-
if (!gateEntry || fullLoaded) {
|
|
913
|
-
setShowSpinner(false);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
const t = setTimeout(() => setShowSpinner(true), 500);
|
|
917
|
-
return () => clearTimeout(t);
|
|
918
|
-
}, [gateEntry, fullLoaded]);
|
|
919
1064
|
const handleClose = useCallback(() => {
|
|
920
1065
|
const reduce = prefersReducedMotion();
|
|
921
|
-
|
|
922
|
-
const thumb = origin && isRectInViewport(origin) ? origin : null;
|
|
923
|
-
const img = imgRef.current;
|
|
924
|
-
entryCleanupRef.current?.();
|
|
1066
|
+
settleEntry();
|
|
925
1067
|
setClosing(true);
|
|
926
1068
|
setVisible(false);
|
|
927
|
-
|
|
928
|
-
const imgRect = img.getBoundingClientRect();
|
|
929
|
-
const wrapper = imgWrapperRef.current;
|
|
930
|
-
if (wrapper) wrapper.style.overflow = "visible";
|
|
931
|
-
setCollapsing(true);
|
|
932
|
-
img.animate(
|
|
933
|
-
[
|
|
934
|
-
{ transformOrigin: "top left", transform: "none" },
|
|
935
|
-
{ transformOrigin: "top left", transform: flipTransform(imgRect, thumb) }
|
|
936
|
-
],
|
|
937
|
-
{ duration: ANIM_MS, easing: ZOOM_EASE, fill: "forwards" }
|
|
938
|
-
);
|
|
939
|
-
}
|
|
1069
|
+
playCollapse();
|
|
940
1070
|
setTimeout(onClose, reduce ? 0 : ANIM_MS);
|
|
941
|
-
}, [onClose,
|
|
1071
|
+
}, [onClose, settleEntry, playCollapse]);
|
|
1072
|
+
const handleBackdropTouchEnd = useCallback(
|
|
1073
|
+
(e) => {
|
|
1074
|
+
if (e.target !== e.currentTarget) return;
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
handleClose();
|
|
1077
|
+
},
|
|
1078
|
+
[handleClose]
|
|
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
|
+
);
|
|
1100
|
+
const handleDoubleClickGuarded = useCallback(
|
|
1101
|
+
(e) => {
|
|
1102
|
+
if (isGhostMouseEvent()) return;
|
|
1103
|
+
handleDoubleClick(e);
|
|
1104
|
+
},
|
|
1105
|
+
[handleDoubleClick, isGhostMouseEvent]
|
|
1106
|
+
);
|
|
1107
|
+
const handleBackdropClick = useCallback(() => {
|
|
1108
|
+
if (isGhostMouseEvent()) return;
|
|
1109
|
+
handleClose();
|
|
1110
|
+
}, [handleClose, isGhostMouseEvent]);
|
|
942
1111
|
const navigate = useCallback(
|
|
943
1112
|
(dir) => {
|
|
944
1113
|
if (dir === "prev") {
|
|
@@ -1018,7 +1187,9 @@ function ImageViewer({
|
|
|
1018
1187
|
const prevItem = prevIndex >= 0 ? items[prevIndex] : null;
|
|
1019
1188
|
const nextItem = nextIndex >= 0 ? items[nextIndex] : null;
|
|
1020
1189
|
const showAdjacent = slideActive || slideAnimating || swipeOffset !== 0;
|
|
1021
|
-
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";
|
|
1022
1193
|
const showZoomCtrls = zoom && !isTouchDevice && showZoomControls && !contentShift.transform;
|
|
1023
1194
|
const headerActions = renderHeaderActions?.(ctx);
|
|
1024
1195
|
const navStart = renderNavStart?.(ctx);
|
|
@@ -1040,7 +1211,8 @@ function ImageViewer({
|
|
|
1040
1211
|
"div",
|
|
1041
1212
|
{
|
|
1042
1213
|
className: cx("rvl-backdrop", cn("backdrop")),
|
|
1043
|
-
onClick: closeOnBackdropClick ?
|
|
1214
|
+
onClick: closeOnBackdropClick ? handleBackdropClick : void 0,
|
|
1215
|
+
onTouchEnd: closeOnBackdropClick ? handleBackdropTouchEnd : void 0,
|
|
1044
1216
|
"aria-hidden": "true"
|
|
1045
1217
|
}
|
|
1046
1218
|
),
|
|
@@ -1049,16 +1221,12 @@ function ImageViewer({
|
|
|
1049
1221
|
/* @__PURE__ */ jsxs("div", { className: "rvl-header-actions", children: [
|
|
1050
1222
|
headerActions,
|
|
1051
1223
|
showZoomCtrls && isZoomed && /* @__PURE__ */ jsxs(
|
|
1052
|
-
|
|
1224
|
+
ChromeButton,
|
|
1053
1225
|
{
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
onClick: (e) => {
|
|
1057
|
-
e.stopPropagation();
|
|
1058
|
-
resetTransform();
|
|
1059
|
-
},
|
|
1226
|
+
className: cx("rvl-btn-scale", cn("button")),
|
|
1227
|
+
onClick: resetTransform,
|
|
1060
1228
|
title: "Reset zoom",
|
|
1061
|
-
|
|
1229
|
+
ariaLabel: "Reset zoom",
|
|
1062
1230
|
children: [
|
|
1063
1231
|
Math.round(displayScale * 100),
|
|
1064
1232
|
"%"
|
|
@@ -1066,44 +1234,32 @@ function ImageViewer({
|
|
|
1066
1234
|
}
|
|
1067
1235
|
),
|
|
1068
1236
|
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1069
|
-
|
|
1237
|
+
ChromeButton,
|
|
1070
1238
|
{
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
onClick: (e) => {
|
|
1074
|
-
e.stopPropagation();
|
|
1075
|
-
ctx.zoomIn();
|
|
1076
|
-
},
|
|
1239
|
+
className: cn("button"),
|
|
1240
|
+
onClick: ctx.zoomIn,
|
|
1077
1241
|
title: "Zoom in",
|
|
1078
|
-
|
|
1242
|
+
ariaLabel: "Zoom in",
|
|
1079
1243
|
children: mergedIcons.zoomIn
|
|
1080
1244
|
}
|
|
1081
1245
|
),
|
|
1082
1246
|
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1083
|
-
|
|
1247
|
+
ChromeButton,
|
|
1084
1248
|
{
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
onClick: (e) => {
|
|
1088
|
-
e.stopPropagation();
|
|
1089
|
-
ctx.zoomOut();
|
|
1090
|
-
},
|
|
1249
|
+
className: cn("button"),
|
|
1250
|
+
onClick: ctx.zoomOut,
|
|
1091
1251
|
title: "Zoom out",
|
|
1092
|
-
|
|
1252
|
+
ariaLabel: "Zoom out",
|
|
1093
1253
|
children: mergedIcons.zoomOut
|
|
1094
1254
|
}
|
|
1095
1255
|
),
|
|
1096
1256
|
/* @__PURE__ */ jsx(
|
|
1097
|
-
|
|
1257
|
+
ChromeButton,
|
|
1098
1258
|
{
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
onClick: (e) => {
|
|
1102
|
-
e.stopPropagation();
|
|
1103
|
-
handleClose();
|
|
1104
|
-
},
|
|
1259
|
+
className: cn("button"),
|
|
1260
|
+
onClick: handleClose,
|
|
1105
1261
|
title: "Close (Esc)",
|
|
1106
|
-
|
|
1262
|
+
ariaLabel: "Close",
|
|
1107
1263
|
children: mergedIcons.close
|
|
1108
1264
|
}
|
|
1109
1265
|
)
|
|
@@ -1113,9 +1269,6 @@ function ImageViewer({
|
|
|
1113
1269
|
"div",
|
|
1114
1270
|
{
|
|
1115
1271
|
className: "rvl-stage",
|
|
1116
|
-
onClick: closeOnBackdropClick ? (e) => {
|
|
1117
|
-
if (e.target === e.currentTarget) handleClose();
|
|
1118
|
-
} : void 0,
|
|
1119
1272
|
style: {
|
|
1120
1273
|
transform: contentShift.transform ?? "translateY(0)",
|
|
1121
1274
|
// animate=false snaps with no transition (overrides the CSS transition)
|
|
@@ -1125,8 +1278,19 @@ function ImageViewer({
|
|
|
1125
1278
|
"div",
|
|
1126
1279
|
{
|
|
1127
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,
|
|
1128
1289
|
className: cx(
|
|
1129
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",
|
|
1130
1294
|
// During a thumbnail zoom the track is opaque from the first frame
|
|
1131
1295
|
// (the image itself is hidden until the zoom starts), so the picture
|
|
1132
1296
|
// flies in crisply instead of cross-fading. On close it only stays
|
|
@@ -1139,7 +1303,11 @@ function ImageViewer({
|
|
|
1139
1303
|
"div",
|
|
1140
1304
|
{
|
|
1141
1305
|
className: "rvl-adjacent",
|
|
1142
|
-
style: {
|
|
1306
|
+
style: {
|
|
1307
|
+
transform: `translateX(${-adjacentOffset}px)`,
|
|
1308
|
+
opacity: swipeOffset > 0 ? adjacentOpacity : 0,
|
|
1309
|
+
transition: adjacentTransition
|
|
1310
|
+
},
|
|
1143
1311
|
children: /* @__PURE__ */ jsx(
|
|
1144
1312
|
"img",
|
|
1145
1313
|
{
|
|
@@ -1158,14 +1326,7 @@ function ImageViewer({
|
|
|
1158
1326
|
ref: imgWrapperRef,
|
|
1159
1327
|
className: "rvl-img-wrapper",
|
|
1160
1328
|
onClick: (e) => e.stopPropagation(),
|
|
1161
|
-
onDoubleClick:
|
|
1162
|
-
onPointerDown: gestures.handlePointerDown,
|
|
1163
|
-
onPointerMove: gestures.handlePointerMove,
|
|
1164
|
-
onPointerUp: gestures.handlePointerUp,
|
|
1165
|
-
onPointerLeave: gestures.handlePointerUp,
|
|
1166
|
-
onTouchStart: gestures.handleTouchStart,
|
|
1167
|
-
onTouchMove: gestures.handleTouchMove,
|
|
1168
|
-
onTouchEnd: gestures.handleTouchEnd,
|
|
1329
|
+
onDoubleClick: handleDoubleClickGuarded,
|
|
1169
1330
|
children: /* @__PURE__ */ jsx(
|
|
1170
1331
|
"img",
|
|
1171
1332
|
{
|
|
@@ -1175,8 +1336,8 @@ function ImageViewer({
|
|
|
1175
1336
|
className: cx("rvl-img", cn("image")),
|
|
1176
1337
|
style: imgStyle,
|
|
1177
1338
|
draggable: false,
|
|
1178
|
-
onLoad:
|
|
1179
|
-
onError:
|
|
1339
|
+
onLoad: onImageLoad,
|
|
1340
|
+
onError: onImageError
|
|
1180
1341
|
}
|
|
1181
1342
|
)
|
|
1182
1343
|
}
|
|
@@ -1186,7 +1347,11 @@ function ImageViewer({
|
|
|
1186
1347
|
"div",
|
|
1187
1348
|
{
|
|
1188
1349
|
className: "rvl-adjacent",
|
|
1189
|
-
style: {
|
|
1350
|
+
style: {
|
|
1351
|
+
transform: `translateX(${adjacentOffset}px)`,
|
|
1352
|
+
opacity: swipeOffset < 0 ? adjacentOpacity : 0,
|
|
1353
|
+
transition: adjacentTransition
|
|
1354
|
+
},
|
|
1190
1355
|
children: /* @__PURE__ */ jsx(
|
|
1191
1356
|
"img",
|
|
1192
1357
|
{
|