uix-image-magnify 0.2.2 → 0.2.4
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 +266 -89
- package/dist/{ImageMagnify-DssaU7jS.d.cts → ImageMagnifyGallery-Bw_SkDZq.d.cts} +35 -18
- package/dist/{ImageMagnify-DssaU7jS.d.ts → ImageMagnifyGallery-Bw_SkDZq.d.ts} +35 -18
- package/dist/index.cjs +128 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -50
- package/dist/index.d.ts +3 -50
- package/dist/index.js +130 -75
- package/dist/index.js.map +1 -1
- package/dist/ssr/index.cjs +406 -4
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +11 -4
- package/dist/ssr/index.d.ts +11 -4
- package/dist/ssr/index.js +407 -7
- package/dist/ssr/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { CSSProperties, ImgHTMLAttributes } from 'react';
|
|
2
|
+
import { CSSProperties, ImgHTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
type ZoomType = "hover" | "click" | "double-click" | "hold" | "wheel" | "none";
|
|
5
5
|
type PanePosition = "left" | "right" | "top" | "bottom";
|
|
@@ -7,35 +7,20 @@ type LensShape = "circle" | "square";
|
|
|
7
7
|
type MagnifyMode = "lens" | "side-by-side" | "inner" | "follow";
|
|
8
8
|
type DragMode = "follow" | "pan";
|
|
9
9
|
interface UseImageMagnifyOptions {
|
|
10
|
-
/** Base image URL (required when used standalone). */
|
|
11
10
|
src?: string;
|
|
12
|
-
/** High-resolution image used for the zoomed view. Defaults to `src`. */
|
|
13
11
|
zoomSrc?: string;
|
|
14
|
-
/** Magnification factor (2 = 200%). */
|
|
15
12
|
zoomScale?: number;
|
|
16
|
-
/** Minimum scale for wheel/keyboard zoom. Default `1.5`. */
|
|
17
13
|
minZoomScale?: number;
|
|
18
|
-
/** Maximum scale for wheel/keyboard zoom. Default `5`. */
|
|
19
14
|
maxZoomScale?: number;
|
|
20
|
-
/** Interaction that activates zoom. */
|
|
21
15
|
zoomType?: ZoomType;
|
|
22
|
-
/** How the zoomed content is displayed. */
|
|
23
16
|
mode?: MagnifyMode;
|
|
24
|
-
/** Drag interaction for `inner` mode. */
|
|
25
17
|
dragMode?: DragMode;
|
|
26
|
-
/** Where the side-by-side pane sits relative to the image. */
|
|
27
18
|
panePosition?: PanePosition;
|
|
28
|
-
/** Diameter / side length of the lens in px. */
|
|
29
19
|
lensSize?: number;
|
|
30
|
-
/** Lens shape. */
|
|
31
20
|
lensShape?: LensShape;
|
|
32
|
-
/** Offset (both axes, px) for `mode="follow"`. */
|
|
33
21
|
followOffset?: number;
|
|
34
|
-
/** Disable all interaction. */
|
|
35
22
|
disabled?: boolean;
|
|
36
|
-
/** Preload the high-res image to avoid a flash. */
|
|
37
23
|
zoomPreload?: boolean;
|
|
38
|
-
/** Reset zoom when the pointer leaves the container. */
|
|
39
24
|
resetOnLeave?: boolean;
|
|
40
25
|
onZoomStart?: () => void;
|
|
41
26
|
onZoomEnd?: () => void;
|
|
@@ -79,10 +64,42 @@ interface ImageMagnifyProps extends UseImageMagnifyOptions {
|
|
|
79
64
|
paneWidth?: number | string;
|
|
80
65
|
paneHeight?: number | string;
|
|
81
66
|
lazy?: boolean;
|
|
82
|
-
imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style">;
|
|
67
|
+
imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style" | "className" | "onLoad" | "onError">;
|
|
83
68
|
onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
|
|
84
69
|
onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
|
|
85
70
|
}
|
|
86
71
|
declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
|
|
87
72
|
|
|
88
|
-
|
|
73
|
+
interface ImageMagnifyGalleryProps extends Omit<ImageMagnifyProps, "src" | "onLoad" | "onError"> {
|
|
74
|
+
images: string[];
|
|
75
|
+
alt: string;
|
|
76
|
+
activeIndex?: number;
|
|
77
|
+
defaultIndex?: number;
|
|
78
|
+
onIndexChange?: (index: number) => void;
|
|
79
|
+
showArrows?: boolean;
|
|
80
|
+
showThumbnails?: boolean;
|
|
81
|
+
showCounter?: boolean;
|
|
82
|
+
loop?: boolean;
|
|
83
|
+
enableKeyboard?: boolean;
|
|
84
|
+
preloadAdjacent?: boolean;
|
|
85
|
+
className?: string;
|
|
86
|
+
style?: CSSProperties;
|
|
87
|
+
viewportClassName?: string;
|
|
88
|
+
viewportStyle?: CSSProperties;
|
|
89
|
+
swiperStyle?: CSSProperties;
|
|
90
|
+
thumbnailContainerClassName?: string;
|
|
91
|
+
thumbnailContainerStyle?: CSSProperties;
|
|
92
|
+
thumbnailClassName?: string;
|
|
93
|
+
thumbnailActiveClassName?: string;
|
|
94
|
+
thumbnailStyle?: CSSProperties;
|
|
95
|
+
thumbnailActiveStyle?: CSSProperties;
|
|
96
|
+
arrowClassName?: string;
|
|
97
|
+
arrowStyle?: CSSProperties;
|
|
98
|
+
thumbnailSize?: number;
|
|
99
|
+
thumbnailGap?: number;
|
|
100
|
+
prevArrow?: ReactNode;
|
|
101
|
+
nextArrow?: ReactNode;
|
|
102
|
+
}
|
|
103
|
+
declare const ImageMagnifyGallery: React.ForwardRefExoticComponent<ImageMagnifyGalleryProps & React.RefAttributes<HTMLDivElement>>;
|
|
104
|
+
|
|
105
|
+
export { type DragMode as D, ImageMagnify as I, type LensShape as L, type MagnifyMode as M, type PanePosition as P, type UseImageMagnifyOptions as U, type ZoomType as Z, ImageMagnifyGallery as a, type ImageMagnifyGalleryProps as b, type ImageMagnifyProps as c, useImageMagnify as u };
|
package/dist/index.cjs
CHANGED
|
@@ -6,7 +6,6 @@ var react$1 = require('swiper/react');
|
|
|
6
6
|
var modules = require('swiper/modules');
|
|
7
7
|
require('swiper/css');
|
|
8
8
|
require('swiper/css/navigation');
|
|
9
|
-
require('swiper/css/thumbs');
|
|
10
9
|
|
|
11
10
|
// src/components/ImageMagnify.tsx
|
|
12
11
|
var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
@@ -82,6 +81,15 @@ function useImageMagnify(options = {}) {
|
|
|
82
81
|
isZoomedRef.current = z;
|
|
83
82
|
setIsZoomed(z);
|
|
84
83
|
if (z) {
|
|
84
|
+
if (modeRef.current === "inner" && dragModeRef.current === "pan") {
|
|
85
|
+
const container = containerRef.current;
|
|
86
|
+
if (container) {
|
|
87
|
+
const rect = container.getBoundingClientRect();
|
|
88
|
+
const s = scaleRef.current;
|
|
89
|
+
txRef.current = rect.width * (1 - s) / 2;
|
|
90
|
+
tyRef.current = rect.height * (1 - s) / 2;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
85
93
|
onZoomStart?.();
|
|
86
94
|
} else {
|
|
87
95
|
if (zoomType === "wheel") {
|
|
@@ -288,8 +296,6 @@ function useImageMagnify(options = {}) {
|
|
|
288
296
|
setZoomed(!isZoomedRef.current);
|
|
289
297
|
} else if (zoomType === "click") {
|
|
290
298
|
setZoomed(!isZoomedRef.current);
|
|
291
|
-
} else if (zoomType === "wheel") {
|
|
292
|
-
setZoomed(!isZoomedRef.current);
|
|
293
299
|
}
|
|
294
300
|
schedule();
|
|
295
301
|
},
|
|
@@ -506,7 +512,8 @@ var ImageMagnify = react.forwardRef(
|
|
|
506
512
|
const showLens = (mode === "lens" || mode === "follow") && isZoomed;
|
|
507
513
|
const showPane = mode === "side-by-side" && isZoomed;
|
|
508
514
|
const showCursor = mode === "side-by-side" && isZoomed;
|
|
509
|
-
const
|
|
515
|
+
const isInteractive = !disabled && zoomType !== "none";
|
|
516
|
+
const cursorStyle = !isInteractive ? "default" : zoomType === "hover" || zoomType === "hold" || zoomType === "wheel" ? "crosshair" : isZoomed ? "zoom-out" : "zoom-in";
|
|
510
517
|
const outerWidth = isMobile ? "100%" : width;
|
|
511
518
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
512
519
|
"div",
|
|
@@ -526,7 +533,7 @@ var ImageMagnify = react.forwardRef(
|
|
|
526
533
|
"div",
|
|
527
534
|
{
|
|
528
535
|
ref: containerRef,
|
|
529
|
-
tabIndex: 0,
|
|
536
|
+
tabIndex: isInteractive ? 0 : -1,
|
|
530
537
|
role: "img",
|
|
531
538
|
"aria-label": alt,
|
|
532
539
|
"aria-disabled": disabled || void 0,
|
|
@@ -693,6 +700,23 @@ var defaultArrowBaseStyle = {
|
|
|
693
700
|
backdropFilter: "blur(4px)",
|
|
694
701
|
WebkitBackdropFilter: "blur(4px)"
|
|
695
702
|
};
|
|
703
|
+
var defaultThumbBaseStyle = {
|
|
704
|
+
flexShrink: 0,
|
|
705
|
+
padding: 0,
|
|
706
|
+
border: "2px solid #e5e5e5",
|
|
707
|
+
background: "#fff",
|
|
708
|
+
cursor: "pointer",
|
|
709
|
+
overflow: "hidden",
|
|
710
|
+
transition: "border-color 0.15s ease, opacity 0.15s ease",
|
|
711
|
+
borderRadius: 2,
|
|
712
|
+
display: "inline-flex",
|
|
713
|
+
alignItems: "center",
|
|
714
|
+
justifyContent: "center",
|
|
715
|
+
boxSizing: "border-box"
|
|
716
|
+
};
|
|
717
|
+
var defaultThumbActiveStyle = {
|
|
718
|
+
borderColor: "#111"
|
|
719
|
+
};
|
|
696
720
|
var visuallyHidden = {
|
|
697
721
|
position: "absolute",
|
|
698
722
|
width: 1,
|
|
@@ -721,6 +745,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
721
745
|
style,
|
|
722
746
|
viewportClassName,
|
|
723
747
|
viewportStyle,
|
|
748
|
+
swiperStyle,
|
|
724
749
|
thumbnailContainerClassName,
|
|
725
750
|
thumbnailContainerStyle,
|
|
726
751
|
thumbnailClassName,
|
|
@@ -737,13 +762,17 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
737
762
|
...magnifyProps
|
|
738
763
|
} = props;
|
|
739
764
|
const [swiper, setSwiper] = react.useState(null);
|
|
740
|
-
const [
|
|
741
|
-
|
|
765
|
+
const [localIndex, setLocalIndex] = react.useState(() => {
|
|
766
|
+
const safeLength = Math.max(1, images.length);
|
|
767
|
+
return Math.max(0, Math.min(safeLength - 1, defaultIndex));
|
|
768
|
+
});
|
|
742
769
|
const prevRef = react.useRef(null);
|
|
743
770
|
const nextRef = react.useRef(null);
|
|
744
771
|
const isControlled = controlledActiveIndex !== void 0;
|
|
745
|
-
const
|
|
772
|
+
const rawIndex = isControlled ? controlledActiveIndex : localIndex;
|
|
773
|
+
const activeIndex = images.length ? Math.max(0, Math.min(images.length - 1, rawIndex)) : 0;
|
|
746
774
|
const hasMultiple = images.length > 1;
|
|
775
|
+
const modules$1 = react.useMemo(() => [modules.Navigation, modules.Keyboard, modules.A11y], []);
|
|
747
776
|
react.useEffect(() => {
|
|
748
777
|
if (!swiper || swiper.destroyed) return;
|
|
749
778
|
if (isControlled && swiper.activeIndex !== controlledActiveIndex) {
|
|
@@ -753,22 +782,31 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
753
782
|
react.useEffect(() => {
|
|
754
783
|
if (!preloadAdjacent || typeof window === "undefined" || images.length <= 1)
|
|
755
784
|
return;
|
|
756
|
-
const targets = /* @__PURE__ */ new Set(
|
|
757
|
-
|
|
758
|
-
(activeIndex
|
|
759
|
-
|
|
785
|
+
const targets = /* @__PURE__ */ new Set();
|
|
786
|
+
if (loop) {
|
|
787
|
+
targets.add((activeIndex + 1) % images.length);
|
|
788
|
+
targets.add((activeIndex - 1 + images.length) % images.length);
|
|
789
|
+
} else {
|
|
790
|
+
if (activeIndex < images.length - 1) targets.add(activeIndex + 1);
|
|
791
|
+
if (activeIndex > 0) targets.add(activeIndex - 1);
|
|
792
|
+
}
|
|
760
793
|
targets.forEach((i) => {
|
|
761
|
-
|
|
794
|
+
const src = images[i];
|
|
795
|
+
if (src) {
|
|
762
796
|
const img = new Image();
|
|
763
|
-
img.src =
|
|
797
|
+
img.src = src;
|
|
764
798
|
}
|
|
765
799
|
});
|
|
766
|
-
}, [activeIndex, images, preloadAdjacent]);
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
}
|
|
800
|
+
}, [activeIndex, images, preloadAdjacent, loop]);
|
|
801
|
+
const handleSlideChange = (s) => {
|
|
802
|
+
const idx = s.activeIndex;
|
|
803
|
+
if (!isControlled) setLocalIndex(idx);
|
|
804
|
+
onIndexChange?.(idx);
|
|
805
|
+
};
|
|
806
|
+
const handleThumbClick = (idx) => {
|
|
807
|
+
if (!swiper || swiper.destroyed) return;
|
|
808
|
+
swiper.slideTo(idx);
|
|
809
|
+
};
|
|
772
810
|
if (images.length === 0) {
|
|
773
811
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className, style });
|
|
774
812
|
}
|
|
@@ -794,36 +832,29 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
794
832
|
modules: modules$1,
|
|
795
833
|
onSwiper: setSwiper,
|
|
796
834
|
initialSlide: isControlled ? controlledActiveIndex : defaultIndex,
|
|
797
|
-
onSlideChange:
|
|
798
|
-
if (!isControlled) setLocalIndex(s.activeIndex);
|
|
799
|
-
onIndexChange?.(s.activeIndex);
|
|
800
|
-
},
|
|
835
|
+
onSlideChange: handleSlideChange,
|
|
801
836
|
onBeforeInit: (s) => {
|
|
802
837
|
if (!showArrows || !hasMultiple) return;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
838
|
+
const nav = s.params.navigation;
|
|
839
|
+
if (nav && typeof nav === "object") {
|
|
840
|
+
nav.prevEl = prevRef.current;
|
|
841
|
+
nav.nextEl = nextRef.current;
|
|
806
842
|
}
|
|
807
843
|
},
|
|
808
844
|
navigation: showArrows && hasMultiple ? {} : false,
|
|
809
|
-
keyboard: enableKeyboard ? { enabled: true } :
|
|
810
|
-
thumbs: {
|
|
811
|
-
swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
|
|
812
|
-
},
|
|
845
|
+
keyboard: enableKeyboard ? { enabled: true } : false,
|
|
813
846
|
loop: loop && hasMultiple,
|
|
814
847
|
autoHeight: true,
|
|
815
848
|
spaceBetween: 10,
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
...arrowStyle
|
|
820
|
-
},
|
|
849
|
+
observer: true,
|
|
850
|
+
observeParents: true,
|
|
851
|
+
style: { width: "100%", ...swiperStyle },
|
|
821
852
|
children: images.map((src, idx) => /* @__PURE__ */ jsxRuntime.jsx(react$1.SwiperSlide, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
822
853
|
ImageMagnify,
|
|
823
854
|
{
|
|
824
855
|
...magnifyProps,
|
|
825
856
|
src,
|
|
826
|
-
alt
|
|
857
|
+
alt: `${alt} \u2014 image ${idx + 1} of ${images.length}`,
|
|
827
858
|
width: "100%"
|
|
828
859
|
}
|
|
829
860
|
) }, idx))
|
|
@@ -836,7 +867,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
836
867
|
ref: prevRef,
|
|
837
868
|
type: "button",
|
|
838
869
|
className: arrowClassName,
|
|
839
|
-
style: { ...defaultArrowBaseStyle, left: 12 },
|
|
870
|
+
style: { ...defaultArrowBaseStyle, left: 12, ...arrowStyle },
|
|
840
871
|
"aria-label": "Previous image",
|
|
841
872
|
children: prevArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon, {})
|
|
842
873
|
}
|
|
@@ -847,7 +878,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
847
878
|
ref: nextRef,
|
|
848
879
|
type: "button",
|
|
849
880
|
className: arrowClassName,
|
|
850
|
-
style: { ...defaultArrowBaseStyle, right: 12 },
|
|
881
|
+
style: { ...defaultArrowBaseStyle, right: 12, ...arrowStyle },
|
|
851
882
|
"aria-label": "Next image",
|
|
852
883
|
children: nextArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowRightIcon, {})
|
|
853
884
|
}
|
|
@@ -887,41 +918,62 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
|
|
|
887
918
|
}
|
|
888
919
|
),
|
|
889
920
|
showThumbnails && hasMultiple && /* @__PURE__ */ jsxRuntime.jsx(
|
|
890
|
-
|
|
921
|
+
"div",
|
|
891
922
|
{
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
nav.nextEl = nextRef.current;
|
|
905
|
-
}
|
|
906
|
-
},
|
|
907
|
-
navigation: showArrows && hasMultiple ? {} : false,
|
|
908
|
-
keyboard: enableKeyboard ? { enabled: true } : void 0,
|
|
909
|
-
thumbs: {
|
|
910
|
-
swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
|
|
923
|
+
className: thumbnailContainerClassName,
|
|
924
|
+
role: "group",
|
|
925
|
+
"aria-label": "Image thumbnails",
|
|
926
|
+
style: {
|
|
927
|
+
display: "flex",
|
|
928
|
+
gap: thumbnailGap,
|
|
929
|
+
marginTop: 12,
|
|
930
|
+
overflowX: "auto",
|
|
931
|
+
overflowY: "hidden",
|
|
932
|
+
paddingBottom: 4,
|
|
933
|
+
scrollbarWidth: "thin",
|
|
934
|
+
...thumbnailContainerStyle
|
|
911
935
|
},
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
936
|
+
children: images.map((src, idx) => {
|
|
937
|
+
const isActive = idx === activeIndex;
|
|
938
|
+
const mergedClassName = [
|
|
939
|
+
thumbnailClassName,
|
|
940
|
+
isActive ? thumbnailActiveClassName : void 0
|
|
941
|
+
].filter(Boolean).join(" ") || void 0;
|
|
942
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
943
|
+
"button",
|
|
944
|
+
{
|
|
945
|
+
type: "button",
|
|
946
|
+
onClick: () => handleThumbClick(idx),
|
|
947
|
+
className: mergedClassName,
|
|
948
|
+
style: {
|
|
949
|
+
...defaultThumbBaseStyle,
|
|
950
|
+
width: thumbnailSize,
|
|
951
|
+
height: thumbnailSize,
|
|
952
|
+
...thumbnailStyle,
|
|
953
|
+
...isActive ? { ...defaultThumbActiveStyle, ...thumbnailActiveStyle } : void 0
|
|
954
|
+
},
|
|
955
|
+
"aria-label": `View image ${idx + 1}`,
|
|
956
|
+
"aria-current": isActive ? "true" : void 0,
|
|
957
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
958
|
+
"img",
|
|
959
|
+
{
|
|
960
|
+
src,
|
|
961
|
+
alt: "",
|
|
962
|
+
loading: "lazy",
|
|
963
|
+
draggable: false,
|
|
964
|
+
style: {
|
|
965
|
+
width: "100%",
|
|
966
|
+
height: "100%",
|
|
967
|
+
objectFit: "cover",
|
|
968
|
+
display: "block",
|
|
969
|
+
pointerEvents: "none"
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
},
|
|
974
|
+
idx
|
|
975
|
+
);
|
|
976
|
+
})
|
|
925
977
|
}
|
|
926
978
|
)
|
|
927
979
|
]
|
|
@@ -936,7 +988,10 @@ function useImageSlider({
|
|
|
936
988
|
onIndexChange,
|
|
937
989
|
loop = true
|
|
938
990
|
}) {
|
|
939
|
-
const [internalIndex, setInternalIndex] = react.useState(
|
|
991
|
+
const [internalIndex, setInternalIndex] = react.useState(() => {
|
|
992
|
+
const safeLength2 = Math.max(1, length);
|
|
993
|
+
return Math.max(0, Math.min(safeLength2 - 1, defaultIndex));
|
|
994
|
+
});
|
|
940
995
|
const safeLength = Math.max(1, length);
|
|
941
996
|
const rawIndex = controlledIndex ?? internalIndex;
|
|
942
997
|
const index = Math.max(0, Math.min(safeLength - 1, rawIndex));
|