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