react-lazy-img-observer 1.2.0 → 1.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/LICENCE.md +21 -21
- package/README.md +146 -64
- package/dist/cjs/ImageLazy.js +49 -23
- package/dist/cjs/ImageLazy.min.js +1 -1
- package/dist/esm/ImageLazy.js +50 -24
- package/dist/esm/ImageLazy.min.js +1 -1
- package/dist/types/ImageLazy.d.ts +13 -2
- package/package.json +2 -1
package/LICENCE.md
CHANGED
@@ -1,21 +1,21 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2023 Percy Saul Rondan Chuzon
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the “Software”), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
13
|
-
all copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
-
THE SOFTWARE.
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Percy Saul Rondan Chuzon
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
-
# 📷 ImageLazy
|
1
|
+
# 📷 ImageLazy (v1.4.0)
|
2
2
|
|
3
|
-
**ImageLazy** is a lightweight React component for lazy loading images using the Intersection Observer API. It
|
3
|
+
**ImageLazy** is a lightweight and customizable React component for lazy loading images using the Intersection Observer API. It includes fallback handling, visual effects, and support for custom loaders and transitions — perfect for performance-oriented applications.
|
4
4
|
|
5
|
-
|
6
|
-

|
5
|
+
**ImageLazy** es un componente liviano y personalizable de React para cargar imágenes perezosamente (lazy load) usando la API de Intersection Observer. Incluye manejo de errores, efectos visuales, y soporte para spinners personalizados y transiciones — ideal para aplicaciones optimizadas.
|
7
6
|
|
8
7
|
---
|
9
8
|
|
10
|
-
## 📦 Installation
|
9
|
+
## 📦 Installation / Instalación
|
11
10
|
|
12
11
|
```bash
|
13
12
|
npm install react-lazy-img-observer
|
@@ -15,116 +14,199 @@ npm install react-lazy-img-observer
|
|
15
14
|
|
16
15
|
---
|
17
16
|
|
18
|
-
## 🚀 Usage
|
17
|
+
## 🚀 Usage / Uso Básico
|
19
18
|
|
20
|
-
|
19
|
+
```tsx
|
20
|
+
import ImageLazy from "react-lazy-img-observer";
|
21
|
+
|
22
|
+
function App() {
|
23
|
+
return (
|
24
|
+
<ImageLazy
|
25
|
+
src="https://example.com/photo.jpg"
|
26
|
+
alt="Example image"
|
27
|
+
width={400}
|
28
|
+
height={300}
|
29
|
+
/>
|
30
|
+
);
|
31
|
+
}
|
32
|
+
```
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
## 🎨 Advanced Example with Spinner and Fallback / Ejemplo Avanzado con Spinner y Fallback
|
21
37
|
|
22
38
|
```tsx
|
23
39
|
import ImageLazy from "react-lazy-img-observer";
|
24
40
|
|
25
|
-
const
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
41
|
+
const Spinner = () => <div className="spinner">Loading...</div>;
|
42
|
+
|
43
|
+
function GalleryImage() {
|
44
|
+
return (
|
45
|
+
<ImageLazy
|
46
|
+
src="/img/cat.webp"
|
47
|
+
alt="Cute Cat"
|
48
|
+
width={300}
|
49
|
+
height={200}
|
50
|
+
fallbackSrc="/img/fallback.webp"
|
51
|
+
backgroundColor="#eee"
|
52
|
+
animationDuration="1s"
|
53
|
+
blurAmount="8px"
|
54
|
+
transitionType="fade"
|
55
|
+
loadingComponent={<Spinner />}
|
56
|
+
/>
|
57
|
+
);
|
58
|
+
}
|
37
59
|
```
|
38
60
|
|
39
|
-
|
61
|
+
---
|
40
62
|
|
41
|
-
|
63
|
+
## 💫 Custom Transition with Your Own CSS / Transiciones Personalizadas con CSS Propio
|
42
64
|
|
43
65
|
```tsx
|
44
66
|
<ImageLazy
|
45
|
-
src="
|
46
|
-
alt="
|
47
|
-
|
48
|
-
|
49
|
-
viewTransitionName="featured-image"
|
67
|
+
src="/img/circle.jpg"
|
68
|
+
alt="Spinning Image"
|
69
|
+
transitionType="custom"
|
70
|
+
className="my-custom-transition"
|
50
71
|
/>
|
51
72
|
```
|
52
73
|
|
53
|
-
|
74
|
+
**CSS**
|
75
|
+
|
76
|
+
```css
|
77
|
+
.my-custom-transition {
|
78
|
+
opacity: 0;
|
79
|
+
transform: rotate(10deg) scale(0.95);
|
80
|
+
transition: all 0.8s ease-in-out;
|
81
|
+
}
|
82
|
+
|
83
|
+
.my-custom-transition.loaded {
|
84
|
+
opacity: 1;
|
85
|
+
transform: rotate(0deg) scale(1);
|
86
|
+
}
|
87
|
+
```
|
54
88
|
|
55
89
|
---
|
56
90
|
|
57
91
|
## 🧩 Props
|
58
92
|
|
59
|
-
| Prop
|
60
|
-
|
|
61
|
-
| `src`
|
62
|
-
| `alt`
|
63
|
-
| `width`
|
64
|
-
| `height`
|
65
|
-
| `
|
66
|
-
| `
|
67
|
-
| `
|
68
|
-
| `
|
69
|
-
| `
|
70
|
-
| `
|
93
|
+
| Prop | Type | Default | Description / Descripción |
|
94
|
+
| -------------------- | ----------------------------------- | ------- | ---------------------------------------------------------- |
|
95
|
+
| `src` | `string` | - | Image URL / URL de imagen ✅ |
|
96
|
+
| `alt` | `string` | - | Alt text / Texto alternativo ✅ |
|
97
|
+
| `width` | `number` | - | Width / Ancho |
|
98
|
+
| `height` | `number` | - | Height / Alto |
|
99
|
+
| `srcSet` | `string` | - | Responsive image set |
|
100
|
+
| `sizes` | `string` | - | Responsive sizes |
|
101
|
+
| `fallbackSrc` | `string` | - | Fallback if image fails / Imagen alternativa si falla |
|
102
|
+
| `backgroundColor` | `string` | - | Background color before load |
|
103
|
+
| `blurAmount` | `string` | `20px` | Initial blur / Desenfoque inicial |
|
104
|
+
| `animationDuration` | `string` | `0.9s` | Transition duration / Duración de transición |
|
105
|
+
| `threshold` | `number` | `0.5` | Intersection threshold / Umbral de visibilidad |
|
106
|
+
| `transitionType` | `"blur"\|"fade"\|"scale"\|"custom"` | `blur` | Type of transition / Tipo de transición |
|
107
|
+
| `loadingComponent` | `ReactNode` | - | Custom loader / Componente de carga personalizado ✅ |
|
108
|
+
| `onLoadComplete` | `() => void` | - | Callback when loaded / Al terminar de cargar |
|
109
|
+
| `visibleByDefault` | `boolean` | `false` | Skip lazy load / Saltar carga diferida si ya está en caché |
|
110
|
+
| `viewTransitionName` | `string` | - | View Transitions API |
|
111
|
+
| `extraData` | `ImgHTMLAttributes` | - | Additional attributes |
|
112
|
+
| `style` | `CSSProperties` | - | Inline styles |
|
113
|
+
| `className` | `string` | - | Custom CSS class |
|
114
|
+
| `id` | `string`\|`number` | - | Element ID |
|
71
115
|
|
72
116
|
---
|
73
117
|
|
74
|
-
##
|
118
|
+
## 🔧 How it works / ¿Cómo funciona?
|
75
119
|
|
76
|
-
|
120
|
+
- When `visibleByDefault` is `false` (default), the image loads only when it's in the viewport (IntersectionObserver).
|
121
|
+
- If `visibleByDefault` is `true`, the image is loaded immediately (useful for SSR or cached images).
|
122
|
+
- You can add a custom CSS transition, fallback, background color, and even a loading spinner.
|
77
123
|
|
78
124
|
---
|
79
125
|
|
80
|
-
##
|
126
|
+
## 🔁 Effects / Efectos Visuales
|
81
127
|
|
82
|
-
|
83
|
-
|
84
|
-
|
128
|
+
| Effect | Description |
|
129
|
+
| -------- | ------------------------------------- |
|
130
|
+
| `blur` | Apply blur and remove it after load |
|
131
|
+
| `fade` | Fade in the image |
|
132
|
+
| `scale` | Slight zoom effect |
|
133
|
+
| `custom` | Use your own CSS (add class to image) |
|
85
134
|
|
86
135
|
---
|
87
136
|
|
88
|
-
##
|
137
|
+
## 🌐 SSR Compatible / Compatible con SSR
|
138
|
+
|
139
|
+
ImageLazy is safe to use in SSR environments (like Next.js). It checks for `window` and disables IntersectionObserver logic when rendering on the server.
|
89
140
|
|
90
|
-
|
141
|
+
ImageLazy es seguro para entornos con renderizado del lado del servidor como Next.js. Detecta si `window` está disponible antes de usar IntersectionObserver.
|
142
|
+
|
143
|
+
---
|
144
|
+
|
145
|
+
## 🖼 Spinner / Carga Personalizada
|
146
|
+
|
147
|
+
You can use `loadingComponent` to display a spinner while the image loads:
|
148
|
+
|
149
|
+
```tsx
|
150
|
+
<ImageLazy
|
151
|
+
src="/img/pic.webp"
|
152
|
+
alt="Custom Spinner"
|
153
|
+
loadingComponent={<div className="my-spinner" />}
|
154
|
+
/>
|
155
|
+
```
|
91
156
|
|
92
157
|
```css
|
93
|
-
.
|
94
|
-
|
95
|
-
|
158
|
+
.my-spinner {
|
159
|
+
width: 40px;
|
160
|
+
height: 40px;
|
161
|
+
border: 4px solid #ccc;
|
162
|
+
border-top-color: #000;
|
163
|
+
border-radius: 50%;
|
164
|
+
animation: spin 1s linear infinite;
|
96
165
|
}
|
97
|
-
|
98
|
-
|
99
|
-
filter: none;
|
166
|
+
@keyframes spin {
|
167
|
+
to { transform: rotate(360deg); }
|
100
168
|
}
|
101
169
|
```
|
102
170
|
|
103
|
-
|
171
|
+
---
|
172
|
+
|
173
|
+
## 📊 Comparison with react-lazy-load-image-component / Comparación con react-lazy-load-image-component
|
174
|
+
|
175
|
+
| Feature / Característica | `ImageLazy` ✅ | `react-lazy-load-image-component` ❌ |
|
176
|
+
|----------------------------------------------|------------------------------------|--------------------------------------------------|
|
177
|
+
| Lightweight & no external dependencies | ✅ Yes / Sí | ❌ No (más pesado y con múltiples componentes) |
|
178
|
+
| SSR Friendly (Next.js compatible) | ✅ Yes / Sí | ✅ Yes / Sí |
|
179
|
+
| Custom transitions via CSS (`custom`) | ✅ Yes / Sí | ❌ No |
|
180
|
+
| Native spinner support (`loadingComponent`) | ✅ Yes / Sí | ❌ No (solo con workarounds) |
|
181
|
+
| Fallback support (`fallbackSrc`) | ✅ Yes / Sí | ✅ Yes / Sí |
|
182
|
+
| Responsive images (`srcSet` and `sizes`) | ✅ Yes / Sí | ✅ Yes / Sí |
|
183
|
+
| Load other elements lazily | ❌ Images only / Solo imágenes | ✅ Yes (via `LazyLoadComponent`) |
|
184
|
+
| Built-in effects | ✅ `blur`, `fade`, `scale`, custom | ✅ `blur`, `opacity`, `bw` |
|
185
|
+
| Transition customization | ✅ Total freedom with CSS | ❌ Limited |
|
104
186
|
|
105
187
|
---
|
106
188
|
|
107
|
-
## 📄 License
|
189
|
+
## 📄 License / Licencia
|
108
190
|
|
109
191
|
[ISC License](./LICENSE)
|
110
192
|
|
111
193
|
---
|
112
194
|
|
113
|
-
##
|
195
|
+
## 👤 Author / Autor
|
114
196
|
|
115
|
-
|
116
|
-
|
197
|
+
**Percy Chuzon**\
|
198
|
+
📧 [contacto@percychuzon.com](mailto\:contacto@percychuzon.com)\
|
199
|
+
🌐 [https://percychuzon.com](https://percychuzon.com)
|
117
200
|
|
118
201
|
---
|
119
202
|
|
120
|
-
##
|
203
|
+
## 💡 Tip
|
121
204
|
|
122
|
-
|
123
|
-
|
124
|
-
|
205
|
+
If you want more control, just set `transitionType="custom"` and style the image with CSS. It’s that simple.
|
206
|
+
|
207
|
+
Si deseas más control, usa `transitionType="custom"` y aplica tus estilos con CSS. Así de simple.
|
125
208
|
|
126
209
|
---
|
127
210
|
|
128
|
-
|
211
|
+
Happy loading! 🎉 / ¡Carga feliz! 🎉
|
129
212
|
|
130
|
-
Thanks to the React community and the MDN docs for inspiration and guidance.
|
package/dist/cjs/ImageLazy.js
CHANGED
@@ -3,39 +3,65 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ImageLazy = void 0;
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
5
5
|
const react_1 = require("react");
|
6
|
-
const ImageLazy = ({ src, alt, width, height, className, id, extraData, title, viewTransitionName, style, }) => {
|
6
|
+
const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.5, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
|
7
7
|
const imageRef = (0, react_1.useRef)(null);
|
8
|
-
const [
|
8
|
+
const [realSrc, setRealSrc] = (0, react_1.useState)(visibleByDefault ? src : null);
|
9
|
+
const [loaded, setLoaded] = (0, react_1.useState)(false);
|
10
|
+
const [hasError, setHasError] = (0, react_1.useState)(false);
|
9
11
|
(0, react_1.useEffect)(() => {
|
10
|
-
if (typeof
|
12
|
+
if (typeof window === "undefined" || visibleByDefault)
|
13
|
+
return;
|
14
|
+
if (typeof IntersectionObserver === "undefined" || !imageRef.current)
|
11
15
|
return;
|
12
|
-
}
|
13
16
|
const observer = new IntersectionObserver((entries) => {
|
14
17
|
entries.forEach((entry) => {
|
15
18
|
if (entry.isIntersecting) {
|
16
|
-
|
17
|
-
if (imageRef.current) {
|
18
|
-
imageRef.current.src = imageRef.current.dataset.src || "";
|
19
|
-
}
|
19
|
+
setRealSrc(src);
|
20
20
|
observer.unobserve(entry.target);
|
21
21
|
}
|
22
22
|
});
|
23
|
-
}, {
|
24
|
-
root: null,
|
25
|
-
rootMargin: "0px",
|
26
|
-
threshold: 0.5,
|
27
|
-
});
|
23
|
+
}, { root: null, rootMargin: "0px", threshold });
|
28
24
|
observer.observe(imageRef.current);
|
29
|
-
return () =>
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
}
|
25
|
+
return () => observer.disconnect();
|
26
|
+
}, [src, threshold, visibleByDefault]);
|
27
|
+
const handleLoad = () => {
|
28
|
+
setLoaded(true);
|
29
|
+
onLoadComplete?.();
|
30
|
+
};
|
31
|
+
const handleError = () => {
|
32
|
+
if (fallbackSrc && realSrc !== fallbackSrc) {
|
33
|
+
setRealSrc(fallbackSrc);
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
setHasError(true);
|
37
|
+
}
|
38
|
+
};
|
39
|
+
const isCustom = transitionType === "custom";
|
40
|
+
const transitionStyles = isCustom
|
41
|
+
? {}
|
42
|
+
: transitionType === "fade"
|
43
|
+
? {
|
44
|
+
opacity: loaded ? 1 : 0,
|
45
|
+
transition: `opacity ${animationDuration}`,
|
46
|
+
}
|
47
|
+
: transitionType === "scale"
|
48
|
+
? {
|
49
|
+
transform: loaded ? "scale(1)" : "scale(1.05)",
|
50
|
+
opacity: loaded ? 1 : 0,
|
51
|
+
transition: `transform ${animationDuration}, opacity ${animationDuration}`,
|
52
|
+
}
|
53
|
+
: {
|
54
|
+
filter: loaded ? "none" : `blur(${blurAmount})`,
|
55
|
+
transition: `filter ${animationDuration}`,
|
56
|
+
};
|
57
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!loaded && loadingComponent, !hasError && ((0, jsx_runtime_1.jsx)("img", { ref: imageRef, src: realSrc ?? "", alt: alt, title: title, className: className, width: width, height: height, loading: "lazy", id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: srcSet, sizes: sizes, style: {
|
58
|
+
backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
|
59
|
+
backgroundSize: "cover",
|
60
|
+
backgroundPosition: "center",
|
61
|
+
...(viewTransitionName ? { viewTransitionName } : {}),
|
62
|
+
...transitionStyles,
|
63
|
+
...style,
|
64
|
+
}, ...extraData }))] }));
|
39
65
|
};
|
40
66
|
exports.ImageLazy = ImageLazy;
|
41
67
|
exports.default = ImageLazy;
|
@@ -1 +1 @@
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ImageLazy=void 0;const jsx_runtime_1=require("react/jsx-runtime"),react_1=require("react"),ImageLazy=({src:e,alt:t,
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ImageLazy=void 0;const jsx_runtime_1=require("react/jsx-runtime"),react_1=require("react"),ImageLazy=({src:e,alt:t,srcSet:r,sizes:n,width:a,height:o,className:i,id:s,extraData:c,title:u,viewTransitionName:l,style:d,backgroundColor:m,animationDuration:f="0.9s",blurAmount:g="20px",fallbackSrc:y,threshold:p=.5,transitionType:_="blur",onLoadComplete:b,visibleByDefault:x=!1,loadingComponent:h})=>{const v=(0,react_1.useRef)(null),[z,j]=(0,react_1.useState)(x?e:null),[I,S]=(0,react_1.useState)(!1),[w,L]=(0,react_1.useState)(!1);(0,react_1.useEffect)((()=>{if("undefined"==typeof window||x)return;if("undefined"==typeof IntersectionObserver||!v.current)return;const t=new IntersectionObserver((r=>{r.forEach((r=>{r.isIntersecting&&(j(e),t.unobserve(r.target))}))}),{root:null,rootMargin:"0px",threshold:p});return t.observe(v.current),()=>t.disconnect()}),[e,p,x]);const k="custom"===_?{}:"fade"===_?{opacity:I?1:0,transition:`opacity ${f}`}:"scale"===_?{transform:I?"scale(1)":"scale(1.05)",opacity:I?1:0,transition:`transform ${f}, opacity ${f}`}:{filter:I?"none":`blur(${g})`,transition:`filter ${f}`};return(0,jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment,{children:[!I&&h,!w&&(0,jsx_runtime_1.jsx)("img",{ref:v,src:z??"",alt:t,title:u,className:i,width:a,height:o,loading:"lazy",id:s?.toString(),onLoad:()=>{S(!0),b?.()},onError:()=>{y&&z!==y?j(y):L(!0)},srcSet:r,sizes:n,style:{backgroundColor:!I&&m?m:void 0,backgroundSize:"cover",backgroundPosition:"center",...l?{viewTransitionName:l}:{},...k,...d},...c})]})};exports.ImageLazy=ImageLazy,exports.default=ImageLazy;
|
package/dist/esm/ImageLazy.js
CHANGED
@@ -1,38 +1,64 @@
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
3
|
-
const ImageLazy = ({ src, alt, width, height, className, id, extraData, title, viewTransitionName, style, }) => {
|
3
|
+
const ImageLazy = ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration = "0.9s", blurAmount = "20px", fallbackSrc, threshold = 0.5, transitionType = "blur", onLoadComplete, visibleByDefault = false, loadingComponent, }) => {
|
4
4
|
const imageRef = useRef(null);
|
5
|
-
const [
|
5
|
+
const [realSrc, setRealSrc] = useState(visibleByDefault ? src : null);
|
6
|
+
const [loaded, setLoaded] = useState(false);
|
7
|
+
const [hasError, setHasError] = useState(false);
|
6
8
|
useEffect(() => {
|
7
|
-
if (typeof
|
9
|
+
if (typeof window === "undefined" || visibleByDefault)
|
10
|
+
return;
|
11
|
+
if (typeof IntersectionObserver === "undefined" || !imageRef.current)
|
8
12
|
return;
|
9
|
-
}
|
10
13
|
const observer = new IntersectionObserver((entries) => {
|
11
14
|
entries.forEach((entry) => {
|
12
15
|
if (entry.isIntersecting) {
|
13
|
-
|
14
|
-
if (imageRef.current) {
|
15
|
-
imageRef.current.src = imageRef.current.dataset.src || "";
|
16
|
-
}
|
16
|
+
setRealSrc(src);
|
17
17
|
observer.unobserve(entry.target);
|
18
18
|
}
|
19
19
|
});
|
20
|
-
}, {
|
21
|
-
root: null,
|
22
|
-
rootMargin: "0px",
|
23
|
-
threshold: 0.5,
|
24
|
-
});
|
20
|
+
}, { root: null, rootMargin: "0px", threshold });
|
25
21
|
observer.observe(imageRef.current);
|
26
|
-
return () =>
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
}
|
22
|
+
return () => observer.disconnect();
|
23
|
+
}, [src, threshold, visibleByDefault]);
|
24
|
+
const handleLoad = () => {
|
25
|
+
setLoaded(true);
|
26
|
+
onLoadComplete?.();
|
27
|
+
};
|
28
|
+
const handleError = () => {
|
29
|
+
if (fallbackSrc && realSrc !== fallbackSrc) {
|
30
|
+
setRealSrc(fallbackSrc);
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
setHasError(true);
|
34
|
+
}
|
35
|
+
};
|
36
|
+
const isCustom = transitionType === "custom";
|
37
|
+
const transitionStyles = isCustom
|
38
|
+
? {}
|
39
|
+
: transitionType === "fade"
|
40
|
+
? {
|
41
|
+
opacity: loaded ? 1 : 0,
|
42
|
+
transition: `opacity ${animationDuration}`,
|
43
|
+
}
|
44
|
+
: transitionType === "scale"
|
45
|
+
? {
|
46
|
+
transform: loaded ? "scale(1)" : "scale(1.05)",
|
47
|
+
opacity: loaded ? 1 : 0,
|
48
|
+
transition: `transform ${animationDuration}, opacity ${animationDuration}`,
|
49
|
+
}
|
50
|
+
: {
|
51
|
+
filter: loaded ? "none" : `blur(${blurAmount})`,
|
52
|
+
transition: `filter ${animationDuration}`,
|
53
|
+
};
|
54
|
+
return (_jsxs(_Fragment, { children: [!loaded && loadingComponent, !hasError && (_jsx("img", { ref: imageRef, src: realSrc ?? "", alt: alt, title: title, className: className, width: width, height: height, loading: "lazy", id: id?.toString(), onLoad: handleLoad, onError: handleError, srcSet: srcSet, sizes: sizes, style: {
|
55
|
+
backgroundColor: !loaded && backgroundColor ? backgroundColor : undefined,
|
56
|
+
backgroundSize: "cover",
|
57
|
+
backgroundPosition: "center",
|
58
|
+
...(viewTransitionName ? { viewTransitionName } : {}),
|
59
|
+
...transitionStyles,
|
60
|
+
...style,
|
61
|
+
}, ...extraData }))] }));
|
36
62
|
};
|
37
63
|
export default ImageLazy;
|
38
64
|
export { ImageLazy };
|
@@ -1 +1 @@
|
|
1
|
-
import{jsx as _jsx}from"react/jsx-runtime";import{useEffect,useRef,useState}from"react";const ImageLazy=({src:e,alt:t,
|
1
|
+
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useRef,useState}from"react";const ImageLazy=({src:e,alt:t,srcSet:r,sizes:n,width:o,height:s,className:a,id:i,extraData:c,title:l,viewTransitionName:u,style:d,backgroundColor:f,animationDuration:m="0.9s",blurAmount:g="20px",fallbackSrc:p,threshold:y=.5,transitionType:b="blur",onLoadComplete:h,visibleByDefault:x=!1,loadingComponent:v})=>{const S=useRef(null),[j,w]=useState(x?e:null),[z,I]=useState(!1),[_,k]=useState(!1);useEffect((()=>{if("undefined"==typeof window||x)return;if("undefined"==typeof IntersectionObserver||!S.current)return;const t=new IntersectionObserver((r=>{r.forEach((r=>{r.isIntersecting&&(w(e),t.unobserve(r.target))}))}),{root:null,rootMargin:"0px",threshold:y});return t.observe(S.current),()=>t.disconnect()}),[e,y,x]);const L="custom"===b?{}:"fade"===b?{opacity:z?1:0,transition:`opacity ${m}`}:"scale"===b?{transform:z?"scale(1)":"scale(1.05)",opacity:z?1:0,transition:`transform ${m}, opacity ${m}`}:{filter:z?"none":`blur(${g})`,transition:`filter ${m}`};return _jsxs(_Fragment,{children:[!z&&v,!_&&_jsx("img",{ref:S,src:j??"",alt:t,title:l,className:a,width:o,height:s,loading:"lazy",id:i?.toString(),onLoad:()=>{I(!0),h?.()},onError:()=>{p&&j!==p?w(p):k(!0)},srcSet:r,sizes:n,style:{backgroundColor:!z&&f?f:void 0,backgroundSize:"cover",backgroundPosition:"center",...u?{viewTransitionName:u}:{},...L,...d},...c})]})};export default ImageLazy;export{ImageLazy};
|
@@ -1,6 +1,8 @@
|
|
1
|
-
type ImagesLazy = {
|
1
|
+
export type ImagesLazy = {
|
2
2
|
src: string;
|
3
3
|
alt: string;
|
4
|
+
srcSet?: string;
|
5
|
+
sizes?: string;
|
4
6
|
width?: number;
|
5
7
|
height?: number;
|
6
8
|
id?: number | string;
|
@@ -9,7 +11,16 @@ type ImagesLazy = {
|
|
9
11
|
extraData?: React.ImgHTMLAttributes<HTMLImageElement>;
|
10
12
|
viewTransitionName?: string;
|
11
13
|
style?: React.CSSProperties;
|
14
|
+
backgroundColor?: string;
|
15
|
+
animationDuration?: string;
|
16
|
+
blurAmount?: string;
|
17
|
+
fallbackSrc?: string;
|
18
|
+
threshold?: number;
|
19
|
+
transitionType?: "blur" | "fade" | "scale" | "custom";
|
20
|
+
onLoadComplete?: () => void;
|
21
|
+
visibleByDefault?: boolean;
|
22
|
+
loadingComponent?: React.ReactNode;
|
12
23
|
};
|
13
|
-
declare const ImageLazy: ({ src, alt, width, height, className, id, extraData, title, viewTransitionName, style, }: ImagesLazy) => import("react/jsx-runtime").JSX.Element;
|
24
|
+
declare const ImageLazy: ({ src, alt, srcSet, sizes, width, height, className, id, extraData, title, viewTransitionName, style, backgroundColor, animationDuration, blurAmount, fallbackSrc, threshold, transitionType, onLoadComplete, visibleByDefault, loadingComponent, }: ImagesLazy) => import("react/jsx-runtime").JSX.Element;
|
14
25
|
export default ImageLazy;
|
15
26
|
export { ImageLazy };
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "react-lazy-img-observer",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.4.0",
|
4
4
|
"description": "A React component for lazy loading images with Intersection Observer",
|
5
5
|
"main": "dist/cjs/ImageLazy.min.js",
|
6
6
|
"module": "dist/esm/ImageLazy.min.js",
|
@@ -37,6 +37,7 @@
|
|
37
37
|
"responsive-images",
|
38
38
|
"web-optimization",
|
39
39
|
"accessibility",
|
40
|
+
"React Lazy Load Image Component",
|
40
41
|
"a11y",
|
41
42
|
"react-hooks",
|
42
43
|
"viewport"
|