@jekrch/react-viewport-lightbox 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -21
- package/dist/index.cjs +240 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -6
- package/dist/index.d.ts +107 -6
- package/dist/index.js +240 -43
- package/dist/index.js.map +1 -1
- package/dist/styles.css +93 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,15 @@ function clampTranslate(x, y, scale, baseDims, viewport) {
|
|
|
19
19
|
y: Math.max(-maxY, Math.min(maxY, y))
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
function zoomToPoint(prevScale, nextScale, prev, focal, viewport) {
|
|
23
|
+
const relX = focal.x - viewport.width / 2;
|
|
24
|
+
const relY = focal.y - viewport.height / 2;
|
|
25
|
+
const k = nextScale / prevScale;
|
|
26
|
+
return {
|
|
27
|
+
x: relX * (1 - k) + k * prev.x,
|
|
28
|
+
y: relY * (1 - k) + k * prev.y
|
|
29
|
+
};
|
|
30
|
+
}
|
|
22
31
|
function resolveSlideDirection({
|
|
23
32
|
offset,
|
|
24
33
|
elapsedMs,
|
|
@@ -39,7 +48,7 @@ function resolveSlideDirection({
|
|
|
39
48
|
// src/hooks/useImageZoomPan.ts
|
|
40
49
|
var MIN_SCALE = 1;
|
|
41
50
|
var MAX_SCALE = 5;
|
|
42
|
-
function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true) {
|
|
51
|
+
function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true, zoomToCursor = true) {
|
|
43
52
|
const imgRef = useRef(null);
|
|
44
53
|
const [displayScale, setDisplayScale] = useState(1);
|
|
45
54
|
const transformRef = useRef({ scale: 1, x: 0, y: 0 });
|
|
@@ -125,12 +134,26 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true) {
|
|
|
125
134
|
const step = -(normalized / 100) * 0.05;
|
|
126
135
|
const factor = 1 + step;
|
|
127
136
|
const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, t.scale * factor));
|
|
128
|
-
|
|
137
|
+
let clamped;
|
|
138
|
+
if (nextScale <= 1) {
|
|
139
|
+
clamped = { x: 0, y: 0 };
|
|
140
|
+
} else if (zoomToCursor) {
|
|
141
|
+
const focal = zoomToPoint(
|
|
142
|
+
t.scale,
|
|
143
|
+
nextScale,
|
|
144
|
+
{ x: t.x, y: t.y },
|
|
145
|
+
{ x: e.clientX, y: e.clientY },
|
|
146
|
+
{ width: window.innerWidth, height: window.innerHeight }
|
|
147
|
+
);
|
|
148
|
+
clamped = clampTranslate2(focal.x, focal.y, nextScale);
|
|
149
|
+
} else {
|
|
150
|
+
clamped = clampTranslate2(t.x, t.y, nextScale);
|
|
151
|
+
}
|
|
129
152
|
setTransform({ scale: nextScale, ...clamped });
|
|
130
153
|
};
|
|
131
154
|
wrapper.addEventListener("wheel", handleWheel, { passive: false });
|
|
132
155
|
return () => wrapper.removeEventListener("wheel", handleWheel);
|
|
133
|
-
}, [imgWrapperRef, setTransform, clampTranslate2, enabled]);
|
|
156
|
+
}, [imgWrapperRef, setTransform, clampTranslate2, enabled, zoomToCursor]);
|
|
134
157
|
const handleDoubleClick = useCallback(
|
|
135
158
|
(e) => {
|
|
136
159
|
if (!enabled) return;
|
|
@@ -161,15 +184,15 @@ function useImageZoomPan(imgWrapperRef, currentIndex, enabled = true) {
|
|
|
161
184
|
handleDoubleClick
|
|
162
185
|
};
|
|
163
186
|
}
|
|
164
|
-
function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart) {
|
|
187
|
+
function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart, loop = false) {
|
|
165
188
|
const slideTrackRef = useRef(null);
|
|
166
189
|
const swipeOffsetRef = useRef(0);
|
|
167
190
|
const [swipeOffset, setSwipeOffset] = useState(0);
|
|
168
191
|
const [slideAnimating, setSlideAnimating] = useState(false);
|
|
169
192
|
const [slideActive, setSlideActive] = useState(false);
|
|
170
193
|
const commitLockRef = useRef(false);
|
|
171
|
-
const hasPrev = currentIndex > 0;
|
|
172
|
-
const hasNext = currentIndex < items.length - 1;
|
|
194
|
+
const hasPrev = loop ? items.length > 1 : currentIndex > 0;
|
|
195
|
+
const hasNext = loop ? items.length > 1 : currentIndex < items.length - 1;
|
|
173
196
|
const applySlideOffset = useCallback((offset, animate = false) => {
|
|
174
197
|
swipeOffsetRef.current = offset;
|
|
175
198
|
const track = slideTrackRef.current;
|
|
@@ -215,7 +238,8 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart) {
|
|
|
215
238
|
if (cleaned) return;
|
|
216
239
|
cleaned = true;
|
|
217
240
|
track?.removeEventListener("transitionend", onTransitionEnd);
|
|
218
|
-
|
|
241
|
+
let newIndex = direction === "prev" ? currentIndex - 1 : currentIndex + 1;
|
|
242
|
+
if (loop) newIndex = (newIndex + items.length) % items.length;
|
|
219
243
|
if (newIndex < 0 || newIndex >= items.length) {
|
|
220
244
|
commitLockRef.current = false;
|
|
221
245
|
return;
|
|
@@ -240,7 +264,7 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart) {
|
|
|
240
264
|
}
|
|
241
265
|
});
|
|
242
266
|
},
|
|
243
|
-
[applySlideOffset, currentIndex, items, onNavigate, onSlideStart]
|
|
267
|
+
[applySlideOffset, currentIndex, items, onNavigate, onSlideStart, loop]
|
|
244
268
|
);
|
|
245
269
|
const resolveSlide = useCallback(
|
|
246
270
|
(gestureStartTime) => {
|
|
@@ -287,7 +311,7 @@ function useSlideNavigation(items, currentIndex, onNavigate, onSlideStart) {
|
|
|
287
311
|
setSlideActive
|
|
288
312
|
};
|
|
289
313
|
}
|
|
290
|
-
function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true) {
|
|
314
|
+
function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true, zoomToCursor = true) {
|
|
291
315
|
const { transformRef, clampTranslate: clampTranslate2, setTransform, applyTransform, resetTransform } = zoomPan;
|
|
292
316
|
const { applySlideOffset, resolveSlide, snapBack, setSlideActive, swipeOffsetRef } = slide;
|
|
293
317
|
const panRef = useRef({
|
|
@@ -442,7 +466,23 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true)
|
|
|
442
466
|
const ratio = dist / p.pinchStartDist;
|
|
443
467
|
const nextScale = Math.min(5, Math.max(1, p.pinchStartScale * ratio));
|
|
444
468
|
const t = transformRef.current;
|
|
445
|
-
|
|
469
|
+
let clamped;
|
|
470
|
+
if (nextScale <= 1) {
|
|
471
|
+
clamped = { x: 0, y: 0 };
|
|
472
|
+
} else if (zoomToCursor) {
|
|
473
|
+
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
474
|
+
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
475
|
+
const focal = zoomToPoint(
|
|
476
|
+
t.scale,
|
|
477
|
+
nextScale,
|
|
478
|
+
{ x: t.x, y: t.y },
|
|
479
|
+
{ x: midX, y: midY },
|
|
480
|
+
{ width: window.innerWidth, height: window.innerHeight }
|
|
481
|
+
);
|
|
482
|
+
clamped = clampTranslate2(focal.x, focal.y, nextScale);
|
|
483
|
+
} else {
|
|
484
|
+
clamped = clampTranslate2(t.x, t.y, nextScale);
|
|
485
|
+
}
|
|
446
486
|
const next = { scale: nextScale, ...clamped };
|
|
447
487
|
transformRef.current = next;
|
|
448
488
|
applyTransform(next);
|
|
@@ -461,7 +501,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true)
|
|
|
461
501
|
updateSlide(touch.clientX, touch.clientY, 6, 0.8);
|
|
462
502
|
}
|
|
463
503
|
},
|
|
464
|
-
[transformRef, clampTranslate2, applyTransform, updateSlide]
|
|
504
|
+
[transformRef, clampTranslate2, applyTransform, updateSlide, zoomToCursor]
|
|
465
505
|
);
|
|
466
506
|
const handleTouchEnd = useCallback(
|
|
467
507
|
(e) => {
|
|
@@ -534,7 +574,7 @@ function useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoomEnabled = true)
|
|
|
534
574
|
function useBarMeasure(topBarRef, bottomBarRef, measureKey) {
|
|
535
575
|
const [topBarH, setTopBarH] = useState(0);
|
|
536
576
|
const [bottomBarH, setBottomBarH] = useState(0);
|
|
537
|
-
|
|
577
|
+
useLayoutEffect(() => {
|
|
538
578
|
const measure = () => {
|
|
539
579
|
if (topBarRef.current) setTopBarH(topBarRef.current.offsetHeight);
|
|
540
580
|
if (bottomBarRef.current) setBottomBarH(bottomBarRef.current.offsetHeight);
|
|
@@ -550,16 +590,29 @@ function useBarMeasure(topBarRef, bottomBarRef, measureKey) {
|
|
|
550
590
|
var lockCount = 0;
|
|
551
591
|
var previousOverflow = "";
|
|
552
592
|
var previousPaddingRight = "";
|
|
593
|
+
var previousPosition = "";
|
|
594
|
+
var previousTop = "";
|
|
595
|
+
var previousWidth = "";
|
|
596
|
+
var lockedScrollY = 0;
|
|
553
597
|
function useBodyScrollLock(isLocked) {
|
|
554
598
|
useEffect(() => {
|
|
555
599
|
if (!isLocked) return;
|
|
556
600
|
if (typeof document === "undefined") return;
|
|
557
601
|
if (lockCount === 0) {
|
|
558
602
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
603
|
+
const rootGutter = window.getComputedStyle(document.documentElement).scrollbarGutter;
|
|
604
|
+
const reservesGutter = typeof rootGutter === "string" && rootGutter.includes("stable");
|
|
605
|
+
lockedScrollY = window.scrollY;
|
|
559
606
|
previousOverflow = document.body.style.overflow;
|
|
560
607
|
previousPaddingRight = document.body.style.paddingRight;
|
|
608
|
+
previousPosition = document.body.style.position;
|
|
609
|
+
previousTop = document.body.style.top;
|
|
610
|
+
previousWidth = document.body.style.width;
|
|
561
611
|
document.body.style.overflow = "hidden";
|
|
562
|
-
|
|
612
|
+
document.body.style.position = "fixed";
|
|
613
|
+
document.body.style.top = `-${lockedScrollY}px`;
|
|
614
|
+
document.body.style.width = "100%";
|
|
615
|
+
if (scrollbarWidth > 0 && !reservesGutter) {
|
|
563
616
|
const currentPaddingRight = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
|
|
564
617
|
document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
565
618
|
}
|
|
@@ -570,6 +623,10 @@ function useBodyScrollLock(isLocked) {
|
|
|
570
623
|
if (lockCount === 0) {
|
|
571
624
|
document.body.style.overflow = previousOverflow;
|
|
572
625
|
document.body.style.paddingRight = previousPaddingRight;
|
|
626
|
+
document.body.style.position = previousPosition;
|
|
627
|
+
document.body.style.top = previousTop;
|
|
628
|
+
document.body.style.width = previousWidth;
|
|
629
|
+
window.scrollTo(0, lockedScrollY);
|
|
573
630
|
}
|
|
574
631
|
};
|
|
575
632
|
}, [isLocked]);
|
|
@@ -682,23 +739,48 @@ function NavButton({ direction, enabled, onClick, icon, className }) {
|
|
|
682
739
|
}
|
|
683
740
|
var ANIM_MS = 250;
|
|
684
741
|
var IMG_PADDING = 44;
|
|
742
|
+
var ZOOM_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
685
743
|
function prefersReducedMotion() {
|
|
686
744
|
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
687
745
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
688
746
|
}
|
|
747
|
+
function flipTransform(from, to) {
|
|
748
|
+
const sx = to.width / from.width;
|
|
749
|
+
const sy = to.height / from.height;
|
|
750
|
+
const dx = to.left - from.left;
|
|
751
|
+
const dy = to.top - from.top;
|
|
752
|
+
return `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
|
|
753
|
+
}
|
|
754
|
+
function canAnimate(el) {
|
|
755
|
+
return !!el && typeof el.animate === "function";
|
|
756
|
+
}
|
|
757
|
+
function isRectInViewport(rect) {
|
|
758
|
+
if (typeof window === "undefined") return true;
|
|
759
|
+
return rect.top < window.innerHeight && rect.top + rect.height > 0 && rect.left < window.innerWidth && rect.left + rect.width > 0;
|
|
760
|
+
}
|
|
689
761
|
function ImageViewer({
|
|
690
762
|
items,
|
|
691
763
|
index,
|
|
692
764
|
onIndexChange,
|
|
693
765
|
onNavigate,
|
|
694
766
|
onClose,
|
|
767
|
+
onEscape,
|
|
768
|
+
getOriginRect,
|
|
695
769
|
zoom = true,
|
|
770
|
+
zoomToCursor = true,
|
|
696
771
|
showCounter = true,
|
|
772
|
+
showZoomControls = true,
|
|
773
|
+
disableNavigation = false,
|
|
697
774
|
loop = false,
|
|
775
|
+
closeOnBackdropClick = false,
|
|
698
776
|
renderHeader,
|
|
699
777
|
renderHeaderActions,
|
|
700
778
|
renderNavStart,
|
|
701
779
|
renderNavEnd,
|
|
780
|
+
navSlotPlacement = "edge",
|
|
781
|
+
navHeight,
|
|
782
|
+
navInset,
|
|
783
|
+
counterFontSize,
|
|
702
784
|
renderFooter,
|
|
703
785
|
renderOverlay,
|
|
704
786
|
classNames,
|
|
@@ -707,6 +789,7 @@ function ImageViewer({
|
|
|
707
789
|
}) {
|
|
708
790
|
const [visible, setVisible] = useState(false);
|
|
709
791
|
const [closing, setClosing] = useState(false);
|
|
792
|
+
const [collapsing, setCollapsing] = useState(false);
|
|
710
793
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
711
794
|
const [contentShift, setContentShiftState] = useState({ transform: null, animate: true });
|
|
712
795
|
const containerRef = useRef(null);
|
|
@@ -726,7 +809,7 @@ function ImageViewer({
|
|
|
726
809
|
useBodyScrollLock(true);
|
|
727
810
|
useFocusTrap(containerRef, visible && !closing);
|
|
728
811
|
const { topBarH, bottomBarH } = useBarMeasure(topBarRef, bottomBarRef, index);
|
|
729
|
-
const zoomPan = useImageZoomPan(imgWrapperRef, index, zoom);
|
|
812
|
+
const zoomPan = useImageZoomPan(imgWrapperRef, index, zoom, zoomToCursor);
|
|
730
813
|
const {
|
|
731
814
|
imgRef,
|
|
732
815
|
displayScale,
|
|
@@ -738,11 +821,19 @@ function ImageViewer({
|
|
|
738
821
|
measureBaseDims,
|
|
739
822
|
handleDoubleClick
|
|
740
823
|
} = zoomPan;
|
|
741
|
-
const slide = useSlideNavigation(items, index, onIndexChange, onNavigate);
|
|
824
|
+
const slide = useSlideNavigation(items, index, onIndexChange, onNavigate, loop);
|
|
742
825
|
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, commitSlide } = slide;
|
|
743
|
-
const gestures = useGestureHandler(zoomPan, slide,
|
|
826
|
+
const gestures = useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoom, zoomToCursor);
|
|
744
827
|
useEffect(() => {
|
|
745
|
-
|
|
828
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
829
|
+
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const mq = window.matchMedia("(hover: none) and (pointer: coarse)");
|
|
833
|
+
const update = () => setIsTouchDevice(mq.matches);
|
|
834
|
+
update();
|
|
835
|
+
mq.addEventListener("change", update);
|
|
836
|
+
return () => mq.removeEventListener("change", update);
|
|
746
837
|
}, []);
|
|
747
838
|
useEffect(() => {
|
|
748
839
|
const onResize = () => setViewportWidth(window.innerWidth);
|
|
@@ -753,43 +844,125 @@ function ImageViewer({
|
|
|
753
844
|
const raf = requestAnimationFrame(() => setVisible(true));
|
|
754
845
|
return () => cancelAnimationFrame(raf);
|
|
755
846
|
}, []);
|
|
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]);
|
|
756
919
|
const handleClose = useCallback(() => {
|
|
920
|
+
const reduce = prefersReducedMotion();
|
|
921
|
+
const origin = !reduce && !isZoomed ? getOriginRect?.(index) ?? null : null;
|
|
922
|
+
const thumb = origin && isRectInViewport(origin) ? origin : null;
|
|
923
|
+
const img = imgRef.current;
|
|
924
|
+
entryCleanupRef.current?.();
|
|
757
925
|
setClosing(true);
|
|
758
926
|
setVisible(false);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
927
|
+
if (thumb && canAnimate(img)) {
|
|
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
|
+
}
|
|
940
|
+
setTimeout(onClose, reduce ? 0 : ANIM_MS);
|
|
941
|
+
}, [onClose, getOriginRect, index, isZoomed, imgRef, imgWrapperRef]);
|
|
762
942
|
const navigate = useCallback(
|
|
763
943
|
(dir) => {
|
|
764
944
|
if (dir === "prev") {
|
|
765
|
-
if (
|
|
766
|
-
else if (loop) {
|
|
767
|
-
onNavigate?.("prev");
|
|
768
|
-
onIndexChange(items.length - 1);
|
|
769
|
-
}
|
|
945
|
+
if (hasPrev) commitSlide("prev");
|
|
770
946
|
} else {
|
|
771
|
-
if (
|
|
772
|
-
else if (loop) {
|
|
773
|
-
onNavigate?.("next");
|
|
774
|
-
onIndexChange(0);
|
|
775
|
-
}
|
|
947
|
+
if (hasNext) commitSlide("next");
|
|
776
948
|
}
|
|
777
949
|
},
|
|
778
|
-
[
|
|
950
|
+
[hasPrev, hasNext, commitSlide]
|
|
779
951
|
);
|
|
780
952
|
useEffect(() => {
|
|
781
953
|
const handler = (e) => {
|
|
782
954
|
if (e.key === "Escape") {
|
|
955
|
+
if (onEscape?.()) return;
|
|
783
956
|
handleClose();
|
|
784
957
|
return;
|
|
785
958
|
}
|
|
786
|
-
if (displayScale > 1) return;
|
|
959
|
+
if (disableNavigation || displayScale > 1) return;
|
|
787
960
|
if (e.key === "ArrowLeft" && hasPrev) navigate("prev");
|
|
788
961
|
if (e.key === "ArrowRight" && hasNext) navigate("next");
|
|
789
962
|
};
|
|
790
963
|
window.addEventListener("keydown", handler);
|
|
791
964
|
return () => window.removeEventListener("keydown", handler);
|
|
792
|
-
}, [handleClose, hasPrev, hasNext, displayScale, navigate]);
|
|
965
|
+
}, [handleClose, hasPrev, hasNext, displayScale, navigate, onEscape, disableNavigation]);
|
|
793
966
|
const setContentShift = useCallback((transform, animate = true) => {
|
|
794
967
|
setContentShiftState({ transform, animate });
|
|
795
968
|
}, []);
|
|
@@ -798,6 +971,7 @@ function ImageViewer({
|
|
|
798
971
|
index,
|
|
799
972
|
item,
|
|
800
973
|
total: items.length,
|
|
974
|
+
closing,
|
|
801
975
|
hasPrev,
|
|
802
976
|
hasNext,
|
|
803
977
|
goPrev: () => navigate("prev"),
|
|
@@ -830,13 +1004,22 @@ function ImageViewer({
|
|
|
830
1004
|
const reservedH = bottomBarH + IMG_PADDING * 2;
|
|
831
1005
|
const imgMaxHeight = `calc(100vh - ${reservedH}px)`;
|
|
832
1006
|
const imgStyle = { maxHeight: imgMaxHeight };
|
|
1007
|
+
const dim = (v) => typeof v === "number" ? `${v}px` : v;
|
|
1008
|
+
const rootStyle = {
|
|
1009
|
+
...navHeight != null && { "--rvl-nav-height": dim(navHeight) },
|
|
1010
|
+
...navInset != null && { "--rvl-nav-inset": dim(navInset) },
|
|
1011
|
+
...counterFontSize != null && { "--rvl-counter-font-size": dim(counterFontSize) }
|
|
1012
|
+
};
|
|
1013
|
+
if (gateEntry && !fullLoaded) imgStyle.opacity = 0;
|
|
833
1014
|
const totalDigits = String(items.length).length;
|
|
834
1015
|
const counterMinWidth = `${totalDigits * 2 * 0.6 + 1.5}em`;
|
|
835
|
-
const
|
|
836
|
-
const
|
|
1016
|
+
const prevIndex = hasPrevLinear ? index - 1 : hasPrev ? items.length - 1 : -1;
|
|
1017
|
+
const nextIndex = hasNextLinear ? index + 1 : hasNext ? 0 : -1;
|
|
1018
|
+
const prevItem = prevIndex >= 0 ? items[prevIndex] : null;
|
|
1019
|
+
const nextItem = nextIndex >= 0 ? items[nextIndex] : null;
|
|
837
1020
|
const showAdjacent = slideActive || slideAnimating || swipeOffset !== 0;
|
|
838
1021
|
const adjacentOpacity = Math.min(1, Math.abs(swipeOffset) / (viewportWidth * 0.8 || 1));
|
|
839
|
-
const
|
|
1022
|
+
const showZoomCtrls = zoom && !isTouchDevice && showZoomControls && !contentShift.transform;
|
|
840
1023
|
const headerActions = renderHeaderActions?.(ctx);
|
|
841
1024
|
const navStart = renderNavStart?.(ctx);
|
|
842
1025
|
const navEnd = renderNavEnd?.(ctx);
|
|
@@ -851,12 +1034,13 @@ function ImageViewer({
|
|
|
851
1034
|
"aria-modal": "true",
|
|
852
1035
|
"aria-label": ariaLabel ?? item.alt ?? "Image viewer",
|
|
853
1036
|
tabIndex: -1,
|
|
1037
|
+
style: rootStyle,
|
|
854
1038
|
children: [
|
|
855
1039
|
/* @__PURE__ */ jsx(
|
|
856
1040
|
"div",
|
|
857
1041
|
{
|
|
858
1042
|
className: cx("rvl-backdrop", cn("backdrop")),
|
|
859
|
-
onClick: handleClose,
|
|
1043
|
+
onClick: closeOnBackdropClick ? handleClose : void 0,
|
|
860
1044
|
"aria-hidden": "true"
|
|
861
1045
|
}
|
|
862
1046
|
),
|
|
@@ -864,7 +1048,7 @@ function ImageViewer({
|
|
|
864
1048
|
/* @__PURE__ */ jsx("div", { className: cx("rvl-header", cn("topBar")), children: renderHeader?.(ctx) }),
|
|
865
1049
|
/* @__PURE__ */ jsxs("div", { className: "rvl-header-actions", children: [
|
|
866
1050
|
headerActions,
|
|
867
|
-
|
|
1051
|
+
showZoomCtrls && isZoomed && /* @__PURE__ */ jsxs(
|
|
868
1052
|
"button",
|
|
869
1053
|
{
|
|
870
1054
|
type: "button",
|
|
@@ -881,7 +1065,7 @@ function ImageViewer({
|
|
|
881
1065
|
]
|
|
882
1066
|
}
|
|
883
1067
|
),
|
|
884
|
-
|
|
1068
|
+
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
885
1069
|
"button",
|
|
886
1070
|
{
|
|
887
1071
|
type: "button",
|
|
@@ -895,7 +1079,7 @@ function ImageViewer({
|
|
|
895
1079
|
children: mergedIcons.zoomIn
|
|
896
1080
|
}
|
|
897
1081
|
),
|
|
898
|
-
|
|
1082
|
+
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
899
1083
|
"button",
|
|
900
1084
|
{
|
|
901
1085
|
type: "button",
|
|
@@ -929,6 +1113,9 @@ function ImageViewer({
|
|
|
929
1113
|
"div",
|
|
930
1114
|
{
|
|
931
1115
|
className: "rvl-stage",
|
|
1116
|
+
onClick: closeOnBackdropClick ? (e) => {
|
|
1117
|
+
if (e.target === e.currentTarget) handleClose();
|
|
1118
|
+
} : void 0,
|
|
932
1119
|
style: {
|
|
933
1120
|
transform: contentShift.transform ?? "translateY(0)",
|
|
934
1121
|
// animate=false snaps with no transition (overrides the CSS transition)
|
|
@@ -938,7 +1125,15 @@ function ImageViewer({
|
|
|
938
1125
|
"div",
|
|
939
1126
|
{
|
|
940
1127
|
ref: slideTrackRef,
|
|
941
|
-
className: cx(
|
|
1128
|
+
className: cx(
|
|
1129
|
+
"rvl-track",
|
|
1130
|
+
// During a thumbnail zoom the track is opaque from the first frame
|
|
1131
|
+
// (the image itself is hidden until the zoom starts), so the picture
|
|
1132
|
+
// flies in crisply instead of cross-fading. On close it only stays
|
|
1133
|
+
// opaque while a FLIP collapse is animating the image back; otherwise
|
|
1134
|
+
// it fades out (so a zoomed close still animates instead of vanishing).
|
|
1135
|
+
(closing ? collapsing : zoomTransition || visible) && "rvl-track-visible"
|
|
1136
|
+
),
|
|
942
1137
|
children: [
|
|
943
1138
|
showAdjacent && prevItem && /* @__PURE__ */ jsx(
|
|
944
1139
|
"div",
|
|
@@ -980,11 +1175,13 @@ function ImageViewer({
|
|
|
980
1175
|
className: cx("rvl-img", cn("image")),
|
|
981
1176
|
style: imgStyle,
|
|
982
1177
|
draggable: false,
|
|
983
|
-
onLoad:
|
|
1178
|
+
onLoad: markFullLoaded,
|
|
1179
|
+
onError: () => setFullLoaded(true)
|
|
984
1180
|
}
|
|
985
1181
|
)
|
|
986
1182
|
}
|
|
987
1183
|
),
|
|
1184
|
+
gateEntry && showSpinner && !fullLoaded && /* @__PURE__ */ jsx("div", { className: cx("rvl-spinner", cn("spinner")), role: "status", "aria-label": "Loading", children: /* @__PURE__ */ jsx("span", { className: "rvl-spinner-ring", "aria-hidden": "true" }) }),
|
|
988
1185
|
showAdjacent && nextItem && /* @__PURE__ */ jsx(
|
|
989
1186
|
"div",
|
|
990
1187
|
{
|
|
@@ -1008,7 +1205,7 @@ function ImageViewer({
|
|
|
1008
1205
|
}
|
|
1009
1206
|
),
|
|
1010
1207
|
/* @__PURE__ */ jsxs("div", { ref: bottomBarRef, className: cx("rvl-bar", "rvl-bottom-bar", cn("bottomBar")), children: [
|
|
1011
|
-
showNavRow && /* @__PURE__ */ jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxs("div", { className: "rvl-nav-inner", children: [
|
|
1208
|
+
showNavRow && /* @__PURE__ */ jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxs("div", { className: cx("rvl-nav-inner", navSlotPlacement === "inline" && "rvl-nav-inline"), children: [
|
|
1012
1209
|
navStart != null && /* @__PURE__ */ jsx("div", { className: cx("rvl-nav-start", cn("navStart")), children: navStart }),
|
|
1013
1210
|
hasNavGroup && /* @__PURE__ */ jsxs("div", { className: "rvl-nav-group", children: [
|
|
1014
1211
|
/* @__PURE__ */ jsx(
|