lazy-svelte-image 1.1.2 → 2.0.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 +237 -32
- package/dist/action/index.d.ts +24 -0
- package/dist/action/index.js +70 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.global.js +140 -0
- package/dist/browser.js +13 -0
- package/dist/component/Image.svelte +390 -61
- package/dist/component/Image.svelte.d.ts +47 -8
- package/dist/element/index.d.ts +30 -0
- package/dist/element/index.js +301 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -1
- package/dist/style.css +159 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/dist/vanilla/index.d.ts +39 -0
- package/dist/vanilla/index.js +317 -0
- package/package.json +58 -25
package/README.md
CHANGED
|
@@ -1,58 +1,263 @@
|
|
|
1
|
-
# lazy-image
|
|
1
|
+
# lazy-svelte-image ⚡
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/lazy-svelte-image)
|
|
4
|
+
[](https://github.com/rishabharidas/lazy-svelte-image/blob/main/LICENSE)
|
|
5
|
+
[](https://svelte.dev)
|
|
6
|
+
[](https://github.com/rishabharidas/lazy-svelte-image)
|
|
7
|
+
[](https://github.com/rishabharidas/lazy-svelte-image)
|
|
3
8
|
|
|
4
|
-
A
|
|
9
|
+
A high-performance, SEO-optimized, **universal** image loading library for **Svelte 5, 4, 3**, **Vanilla JavaScript**, and **all modern frameworks (React, Vue, Angular, Solid)** via HTML5 Web Components or standalone CDN `<script>`.
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
---
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
## 🌟 Highlights
|
|
14
|
+
|
|
15
|
+
- ⚡ **Universal & Framework-Agnostic**: Works as a Svelte component, Svelte action (`use:lazyImage`), HTML5 Web Component (`<lazy-image>`), Vanilla JS class (`LazyImage`), or direct CDN script tag.
|
|
16
|
+
- 🚀 **Svelte 5 & Backwards Compatible**: Runs natively on the latest Svelte 5 as well as Svelte 4 and Svelte 3.
|
|
17
|
+
- 🔒 **Zero Vulnerabilities**: Completely modernized dependency tree with 0 security advisories.
|
|
18
|
+
- 🔍 **SEO & Core Web Vitals Optimized**:
|
|
19
|
+
- **SSR & Crawler Indexing**: Googlebot, Bing, Pinterest, and social crawlers discover images immediately on initial page load via SSR-friendly rendering and automatic `<noscript>` fallbacks.
|
|
20
|
+
- **Zero Cumulative Layout Shift (CLS)**: Built-in `aspectRatio` space reservation prevents jarring content shifts.
|
|
21
|
+
- **Schema.org Structured Data**: Automatic Google-compliant `ImageObject` JSON-LD generation for image search previews.
|
|
22
|
+
- **LCP Optimization**: Supports `fetchpriority="high" | "low"` and `decoding="async"`.
|
|
23
|
+
- 🎨 **Blur-Up LQIP**: Low-Quality Image Placeholder support with smooth, customizable blur transitions.
|
|
24
|
+
- 🔁 **Error Recovery & Retries**: Built-in broken image indicator with customizable slots and interactive retry button.
|
|
25
|
+
- 📦 **Zero-Config CDN**: Drop-in `<script>` tag for WordPress, PHP, Hugo, or plain HTML websites.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 📦 Installation
|
|
9
30
|
|
|
10
31
|
```bash
|
|
11
|
-
npm
|
|
32
|
+
npm install lazy-svelte-image
|
|
12
33
|
```
|
|
13
34
|
|
|
14
|
-
|
|
35
|
+
_(Note: Svelte is an optional peer dependency. You can install this package in Vanilla JS or React/Vue projects without peer dependency warnings!)_
|
|
15
36
|
|
|
16
|
-
|
|
37
|
+
---
|
|
17
38
|
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
## 🚀 Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Svelte Component
|
|
42
|
+
|
|
43
|
+
```svelte
|
|
44
|
+
<script>
|
|
45
|
+
import { Image } from 'lazy-svelte-image';
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<!-- High-performance image with blur-up LQIP and zero CLS -->
|
|
49
|
+
<Image
|
|
50
|
+
src="https://images.example.com/landscape.jpg"
|
|
51
|
+
placeholder="https://images.example.com/landscape-thumb.jpg"
|
|
52
|
+
alt="Majestic mountain lake at sunrise"
|
|
53
|
+
aspectRatio="16/9"
|
|
54
|
+
caption="Rocky Mountains, Colorado"
|
|
55
|
+
schema={true}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### With Custom Slots and Error Retry:
|
|
60
|
+
|
|
61
|
+
```svelte
|
|
62
|
+
<Image src="/path/to/image.jpg" aspectRatio="4/3" retry={true} maxRetries={3}>
|
|
63
|
+
<!-- Custom spinner -->
|
|
64
|
+
<div slot="loader" class="my-custom-spinner">Loading...</div>
|
|
65
|
+
|
|
66
|
+
<!-- Custom broken state -->
|
|
67
|
+
<div slot="broken" class="my-custom-error">
|
|
68
|
+
<span>Oops! Image could not be loaded.</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Custom overlay watermark or badge -->
|
|
72
|
+
<span slot="overlay" class="badge">PRO</span>
|
|
73
|
+
</Image>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 2. Svelte Action (`use:lazyImage`)
|
|
79
|
+
|
|
80
|
+
For minimalists who want zero wrapper elements around a native `<img>`:
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<script>
|
|
84
|
+
import { lazyImage } from 'lazy-svelte-image';
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<img
|
|
88
|
+
use:lazyImage={{
|
|
89
|
+
src: 'https://images.example.com/photo.jpg',
|
|
90
|
+
placeholder: 'https://images.example.com/photo-thumb.jpg',
|
|
91
|
+
rootMargin: '200px'
|
|
92
|
+
}}
|
|
93
|
+
alt="Modern living room interior"
|
|
94
|
+
style="aspect-ratio: 16/9; width: 100%; object-fit: cover;"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
20
99
|
|
|
100
|
+
### 3. HTML5 Web Component (`<lazy-image>`)
|
|
21
101
|
|
|
22
|
-
|
|
102
|
+
Works in **React**, **Vue**, **Angular**, **Solid**, or plain HTML without requiring any Svelte runtime!
|
|
23
103
|
|
|
24
|
-
|
|
104
|
+
```html
|
|
105
|
+
<!-- Import the web component -->
|
|
106
|
+
<script type="module">
|
|
107
|
+
import 'lazy-svelte-image/element';
|
|
108
|
+
</script>
|
|
25
109
|
|
|
26
|
-
<
|
|
110
|
+
<lazy-image
|
|
111
|
+
src="https://images.example.com/nature.jpg"
|
|
112
|
+
placeholder="https://images.example.com/nature-thumb.jpg"
|
|
113
|
+
alt="Autumn forest path"
|
|
114
|
+
aspect-ratio="16/9"
|
|
115
|
+
caption="Autumn foliage"
|
|
116
|
+
retry
|
|
117
|
+
></lazy-image>
|
|
27
118
|
```
|
|
28
119
|
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### 4. Vanilla JavaScript
|
|
123
|
+
|
|
124
|
+
Framework-free imperative API:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
import { createLazyImage } from 'lazy-svelte-image/vanilla';
|
|
128
|
+
import 'lazy-svelte-image/style.css';
|
|
129
|
+
|
|
130
|
+
const imageInstance = createLazyImage('#my-image-container', {
|
|
131
|
+
src: 'https://images.example.com/hero.jpg',
|
|
132
|
+
placeholder: 'https://images.example.com/hero-lqip.jpg',
|
|
133
|
+
alt: 'Hero banner',
|
|
134
|
+
aspectRatio: '16/9',
|
|
135
|
+
schema: true,
|
|
136
|
+
retry: true,
|
|
137
|
+
onLoad: (event) => console.log('Image loaded!', event),
|
|
138
|
+
onError: (error) => console.error('Image failed', error)
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### 5. Global CDN Usage (No bundler required)
|
|
145
|
+
|
|
146
|
+
Add directly to any traditional website (WordPress, PHP, Shopify, plain HTML):
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<!-- Include stylesheet and script -->
|
|
150
|
+
<link rel="stylesheet" href="https://unpkg.com/lazy-svelte-image/dist/style.css" />
|
|
151
|
+
<script src="https://unpkg.com/lazy-svelte-image/dist/browser.global.js"></script>
|
|
152
|
+
|
|
153
|
+
<!-- Use the custom element immediately -->
|
|
154
|
+
<lazy-image
|
|
155
|
+
src="https://images.example.com/photo.jpg"
|
|
156
|
+
aspect-ratio="16/9"
|
|
157
|
+
alt="Example image"
|
|
158
|
+
></lazy-image>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🔍 SEO & Core Web Vitals Guide
|
|
164
|
+
|
|
165
|
+
### 1. Cumulative Layout Shift (CLS = 0)
|
|
166
|
+
|
|
167
|
+
When an image doesn't have an explicit size or aspect ratio, the browser cannot reserve vertical space while loading, causing the page content to jump when the image renders.
|
|
168
|
+
By specifying `aspectRatio="16/9"` or `aspectRatio="4/3"`, `lazy-svelte-image` enforces CSS aspect-ratio on the container before the image arrives, ensuring **zero layout shift**.
|
|
169
|
+
|
|
170
|
+
### 2. SSR & Search Engine Indexing
|
|
171
|
+
|
|
172
|
+
Older lazy loaders hide the `<img>` element during SSR until hydration `onMount`, resulting in web crawlers indexing nothing.
|
|
173
|
+
`lazy-svelte-image` outputs:
|
|
174
|
+
|
|
175
|
+
1. Crawler-friendly attributes on initial HTML render.
|
|
176
|
+
2. An automatic `<noscript><img src="..." alt="..." /></noscript>` block ensuring 100% indexing even if JavaScript execution is delayed or disabled.
|
|
177
|
+
|
|
178
|
+
### 3. Schema.org Structured Data
|
|
179
|
+
|
|
180
|
+
Setting `schema={true}` automatically injects Google-compliant `ImageObject` JSON-LD metadata into the page for enhanced Google Images visibility:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"@context": "https://schema.org",
|
|
185
|
+
"@type": "ImageObject",
|
|
186
|
+
"contentUrl": "https://images.example.com/photo.jpg",
|
|
187
|
+
"url": "https://images.example.com/photo.jpg",
|
|
188
|
+
"name": "Alt description",
|
|
189
|
+
"description": "Caption or Alt text",
|
|
190
|
+
"width": "1920",
|
|
191
|
+
"height": "1080"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 🛠️ API Reference
|
|
198
|
+
|
|
199
|
+
### Component Props (`<Image />`)
|
|
200
|
+
|
|
201
|
+
| Prop | Type | Default | Description |
|
|
202
|
+
| :---------------- | :--------------------------------------------------------- | :------------ | :---------------------------------------------------------------- |
|
|
203
|
+
| `src` | `string` | _(Required)_ | URL of the high-resolution image |
|
|
204
|
+
| `alt` | `string` | `''` | Descriptive alternative text for accessibility and SEO |
|
|
205
|
+
| `placeholder` | `string` | `undefined` | Low-resolution image or data URI for LQIP blur-up |
|
|
206
|
+
| `aspectRatio` | `string \| number` | `undefined` | CSS aspect ratio (e.g. `'16/9'`, `'4/3'`, `'1/1'`) to prevent CLS |
|
|
207
|
+
| `width` | `string \| number` | `undefined` | Width of the image container |
|
|
208
|
+
| `height` | `string \| number` | `undefined` | Height of the image container |
|
|
209
|
+
| `srcset` | `string` | `undefined` | Responsive image candidates (`srcset`) |
|
|
210
|
+
| `sizes` | `string` | `undefined` | Responsive image sizes rule (`sizes`) |
|
|
211
|
+
| `sources` | `PictureSource[]` | `[]` | Array of source definitions for `<picture>` elements |
|
|
212
|
+
| `objectFit` | `'cover' \| 'contain' \| 'fill' \| 'none' \| 'scale-down'` | `'cover'` | CSS `object-fit` property |
|
|
213
|
+
| `objectPosition` | `string` | `'center'` | CSS `object-position` property |
|
|
214
|
+
| `rootMargin` | `string` | `'200px'` | Preload margin for `IntersectionObserver` |
|
|
215
|
+
| `threshold` | `number \| number[]` | `0.01` | Intersection threshold |
|
|
216
|
+
| `native` | `boolean` | `false` | Use browser's native `loading="lazy"` |
|
|
217
|
+
| `fetchpriority` | `'high' \| 'low' \| 'auto'` | `undefined` | Resource priority hint (useful for LCP images) |
|
|
218
|
+
| `decoding` | `'async' \| 'sync' \| 'auto'` | `'async'` | Image decoding mode |
|
|
219
|
+
| `fadeDuration` | `number` | `300` | Fade-in transition duration in milliseconds |
|
|
220
|
+
| `blur` | `number` | `12` | Blur radius in pixels for the LQIP placeholder |
|
|
221
|
+
| `backgroundColor` | `string` | `'#c2c2c224'` | Placeholder background color |
|
|
222
|
+
| `disableLoader` | `boolean` | `false` | Disable the loading spinner |
|
|
223
|
+
| `disabeLoader` | `boolean` | `false` | Backward-compatible alias for `disableLoader` |
|
|
224
|
+
| `disableBroken` | `boolean` | `false` | Disable broken image fallback view |
|
|
225
|
+
| `retry` | `boolean` | `false` | Display retry button on failure |
|
|
226
|
+
| `maxRetries` | `number` | `2` | Maximum retry attempts |
|
|
227
|
+
| `caption` | `string` | `undefined` | Optional figure caption |
|
|
228
|
+
| `title` | `string` | `undefined` | Image title attribute |
|
|
229
|
+
| `schema` | `boolean \| object` | `false` | Generate Schema.org `ImageObject` JSON-LD |
|
|
230
|
+
|
|
231
|
+
### Slots
|
|
29
232
|
|
|
30
|
-
|
|
233
|
+
| Slot Name | Description |
|
|
234
|
+
| :-------- | :------------------------------------------------------- |
|
|
235
|
+
| `loader` | Custom loading indicator / spinner |
|
|
236
|
+
| `broken` | Custom broken image fallback UI |
|
|
237
|
+
| `overlay` | Custom overlay element (e.g. badge, watermark, controls) |
|
|
238
|
+
| `caption` | Custom caption markup inside `<figcaption>` |
|
|
31
239
|
|
|
32
|
-
|
|
240
|
+
### Events
|
|
33
241
|
|
|
34
|
-
|
|
242
|
+
| Event | Detail | Description |
|
|
243
|
+
| :---------- | :------------------------------------- | :-------------------------------------------------------- |
|
|
244
|
+
| `load` | `{ event: Event }` | Fired when the high-resolution image has finished loading |
|
|
245
|
+
| `error` | `{ event: Event }` | Fired when the image fails to load |
|
|
246
|
+
| `intersect` | `{ entry: IntersectionObserverEntry }` | Fired when the image enters the observer viewport |
|
|
247
|
+
| `retry` | `{ attempt: number }` | Fired when a retry attempt is triggered |
|
|
35
248
|
|
|
36
|
-
|
|
37
|
-
- <code>disableBroken</code> - disable all broken view
|
|
38
|
-
- <code>backgroundColor</code> - set custom background color for default broken/loader
|
|
249
|
+
---
|
|
39
250
|
|
|
40
|
-
|
|
251
|
+
## 🔄 Backward Compatibility with v1.x
|
|
41
252
|
|
|
42
|
-
|
|
253
|
+
This version is **100% backward compatible** with `lazy-svelte-image` v1.1.2:
|
|
43
254
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</span>
|
|
48
|
-
```
|
|
49
|
-
#### For custom Loaders
|
|
255
|
+
- Existing props `src`, `alt`, `backgroundColor`, `disableLoader`, `disabeLoader` (original typo alias), and `disableBroken` are fully supported.
|
|
256
|
+
- Existing slots `slot="loader"` and `slot="broken"` work without any code changes.
|
|
257
|
+
- Existing projects upgrading from Svelte 4 to Svelte 5 will work without modifications.
|
|
50
258
|
|
|
51
|
-
|
|
52
|
-
<span slot="loader">
|
|
53
|
-
<!-- provide loader code here -->
|
|
54
|
-
</span>
|
|
55
|
-
```
|
|
259
|
+
---
|
|
56
260
|
|
|
261
|
+
## 📄 License
|
|
57
262
|
|
|
58
|
-
|
|
263
|
+
MIT © [Rishabh Haridas](https://github.com/rishabharidas)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LazyImageActionOptions {
|
|
2
|
+
src: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
srcset?: string;
|
|
5
|
+
sizes?: string;
|
|
6
|
+
rootMargin?: string;
|
|
7
|
+
threshold?: number | number[];
|
|
8
|
+
native?: boolean;
|
|
9
|
+
fadeDuration?: number;
|
|
10
|
+
onLoad?: (e: Event) => void;
|
|
11
|
+
onError?: (e: Event) => void;
|
|
12
|
+
onIntersect?: (entry: IntersectionObserverEntry) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Svelte Action `use:lazyImage`
|
|
16
|
+
* Works across Svelte 3, Svelte 4, and Svelte 5!
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* <img use:lazyImage={{ src: 'large.jpg', placeholder: 'thumb.jpg' }} alt="Hero" />
|
|
20
|
+
*/
|
|
21
|
+
export declare function lazyImage(node: HTMLImageElement, options: LazyImageActionOptions): {
|
|
22
|
+
update(newOptions: LazyImageActionOptions): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { observeElement } from '../vanilla/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Svelte Action `use:lazyImage`
|
|
4
|
+
* Works across Svelte 3, Svelte 4, and Svelte 5!
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* <img use:lazyImage={{ src: 'large.jpg', placeholder: 'thumb.jpg' }} alt="Hero" />
|
|
8
|
+
*/
|
|
9
|
+
export function lazyImage(node, options) {
|
|
10
|
+
let currentOptions = { ...options };
|
|
11
|
+
let unobserve = null;
|
|
12
|
+
const fadeDuration = currentOptions.fadeDuration ?? 300;
|
|
13
|
+
node.style.transition = `opacity ${fadeDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
|
|
14
|
+
if (currentOptions.placeholder) {
|
|
15
|
+
node.src = currentOptions.placeholder;
|
|
16
|
+
node.style.opacity = '0.6';
|
|
17
|
+
node.style.filter = 'blur(8px)';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
node.style.opacity = '0';
|
|
21
|
+
}
|
|
22
|
+
function load() {
|
|
23
|
+
const img = new Image();
|
|
24
|
+
if (currentOptions.srcset)
|
|
25
|
+
img.srcset = currentOptions.srcset;
|
|
26
|
+
if (currentOptions.sizes)
|
|
27
|
+
img.sizes = currentOptions.sizes;
|
|
28
|
+
img.src = currentOptions.src;
|
|
29
|
+
img.onload = (e) => {
|
|
30
|
+
node.src = currentOptions.src;
|
|
31
|
+
if (currentOptions.srcset)
|
|
32
|
+
node.srcset = currentOptions.srcset;
|
|
33
|
+
if (currentOptions.sizes)
|
|
34
|
+
node.sizes = currentOptions.sizes;
|
|
35
|
+
node.style.opacity = '1';
|
|
36
|
+
node.style.filter = 'none';
|
|
37
|
+
node.dispatchEvent(new CustomEvent('lazyload', { detail: { event: e } }));
|
|
38
|
+
currentOptions.onLoad?.(e);
|
|
39
|
+
};
|
|
40
|
+
img.onerror = (e) => {
|
|
41
|
+
node.dispatchEvent(new CustomEvent('lazyerror', { detail: { event: e } }));
|
|
42
|
+
currentOptions.onError?.(e);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (currentOptions.native) {
|
|
46
|
+
node.loading = 'lazy';
|
|
47
|
+
load();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
unobserve = observeElement(node, (entry) => {
|
|
51
|
+
node.dispatchEvent(new CustomEvent('lazyintersect', { detail: { entry } }));
|
|
52
|
+
currentOptions.onIntersect?.(entry);
|
|
53
|
+
load();
|
|
54
|
+
}, {
|
|
55
|
+
rootMargin: currentOptions.rootMargin,
|
|
56
|
+
threshold: currentOptions.threshold
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
update(newOptions) {
|
|
61
|
+
currentOptions = { ...newOptions };
|
|
62
|
+
},
|
|
63
|
+
destroy() {
|
|
64
|
+
if (unobserve) {
|
|
65
|
+
unobserve();
|
|
66
|
+
unobserve = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { LazyImage, createLazyImage, createImageSchema, SPINNER_SVG, BROKEN_SVG } from './vanilla/index.js';
|
|
2
|
+
import { LazyImageElement, defineLazyImageElement } from './element/index.js';
|
|
3
|
+
export { LazyImage, createLazyImage, createImageSchema, LazyImageElement, defineLazyImageElement, SPINNER_SVG, BROKEN_SVG };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var LazyImageGlobal=(function(d){"use strict";var T=Object.defineProperty;var H=(d,h,m)=>h in d?T(d,h,{enumerable:!0,configurable:!0,writable:!0,value:m}):d[h]=m;var o=(d,h,m)=>H(d,typeof h!="symbol"?h+"":h,m);const h='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lazy-image-spinner"><circle stroke-dasharray="113.097 39.699" r="24" stroke-width="4" stroke="currentColor" fill="none" cy="50" cx="50"></circle></svg>',m='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="lazy-image-broken-icon"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM116.6 187.9L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9zM220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3L220.9 270z"/></svg>';function u(a){const t={"@context":"https://schema.org","@type":"ImageObject",contentUrl:a.src,url:a.src,name:a.alt||a.title||"Image",description:a.alt||a.caption||""};return a.width&&(t.width=String(a.width)),a.height&&(t.height=String(a.height)),a.caption&&(t.caption=a.caption),typeof a.schema=="object"&&a.schema!==null&&Object.assign(t,a.schema),JSON.stringify(t)}const v=new Map,g=new Map;function x(a,t){return`${a}_${Array.isArray(t)?t.join(","):t}`}function E(a,t,i={}){if(typeof window>"u"||!("IntersectionObserver"in window))return t({isIntersecting:!0,target:a}),()=>{};const r=i.rootMargin||"200px",n=i.threshold??.01,e=x(r,n);let s=v.get(e);return s||(s=new IntersectionObserver(l=>{for(const c of l)if(c.isIntersecting){const p=g.get(c.target);p&&(p(c),s==null||s.unobserve(c.target),g.delete(c.target))}},{rootMargin:r,threshold:n}),v.set(e,s)),g.set(a,t),s.observe(a),()=>{s==null||s.unobserve(a),g.delete(a)}}class y{constructor(t,i){o(this,"container");o(this,"options");o(this,"state","idle");o(this,"unobserve",null);o(this,"currentAttempt",0);o(this,"wrapperEl",null);o(this,"placeholderImgEl",null);o(this,"mainImgEl",null);o(this,"pictureEl",null);o(this,"overlayEl",null);o(this,"schemaScriptEl",null);const r=typeof t=="string"?document.querySelector(t):t;if(!r||!(r instanceof HTMLElement))throw new Error("[LazyImage] Invalid target element or selector.");this.container=r,this.options={...i},this.init()}init(){this.renderStructure(),this.options.native?this.loadMainImage():this.unobserve=E(this.container,t=>{var i,r;(r=(i=this.options).onIntersect)==null||r.call(i,t),this.loadMainImage()},{rootMargin:this.options.rootMargin,threshold:this.options.threshold})}renderStructure(){this.container.innerHTML="";const t=this.options,i=document.createElement("div");if(i.className="lazy-image-wrapper",t.backgroundColor&&i.style.setProperty("--lazy-bg",t.backgroundColor),t.fadeDuration!==void 0&&i.style.setProperty("--lazy-fade",`${t.fadeDuration}ms`),t.blur!==void 0&&i.style.setProperty("--lazy-blur",`${t.blur}px`),t.objectFit&&i.style.setProperty("--lazy-object-fit",t.objectFit),t.objectPosition&&i.style.setProperty("--lazy-object-position",t.objectPosition),t.aspectRatio&&(i.setAttribute("data-aspect-ratio","true"),i.style.setProperty("--lazy-aspect-ratio",String(t.aspectRatio))),t.width&&(i.style.width=typeof t.width=="number"?`${t.width}px`:t.width),t.height&&(i.style.height=typeof t.height=="number"?`${t.height}px`:t.height),this.wrapperEl=i,t.placeholder){const e=document.createElement("img");e.className="lazy-image-placeholder-img",e.src=t.placeholder,e.alt="",e.setAttribute("aria-hidden","true"),e.decoding="async",i.appendChild(e),this.placeholderImgEl=e}const r=document.createElement("div");if(r.className="lazy-image-overlay",i.appendChild(r),this.overlayEl=r,(t.disableLoader??t.disabeLoader??!1)||(r.innerHTML=`<div class="lazy-image-loader-container">${h}</div>`),t.sources&&t.sources.length>0){const e=document.createElement("picture");e.className="lazy-image-picture";for(const l of t.sources){const c=document.createElement("source");c.srcset=l.srcset,l.type&&(c.type=l.type),l.media&&(c.media=l.media),l.sizes&&(c.sizes=l.sizes),e.appendChild(c)}const s=document.createElement("img");s.className="lazy-image-main",s.alt=t.alt||"",t.title&&(s.title=t.title),t.native&&(s.loading="lazy",s.src=t.src,t.srcset&&(s.srcset=t.srcset),t.sizes&&(s.sizes=t.sizes)),s.decoding=t.decoding||"async",t.fetchpriority&&s.setAttribute("fetchpriority",t.fetchpriority),e.appendChild(s),i.appendChild(e),this.pictureEl=e,this.mainImgEl=s}else{const e=document.createElement("img");e.className="lazy-image-main",e.alt=t.alt||"",t.title&&(e.title=t.title),t.native&&(e.loading="lazy",e.src=t.src,t.srcset&&(e.srcset=t.srcset),t.sizes&&(e.sizes=t.sizes)),e.decoding=t.decoding||"async",t.fetchpriority&&e.setAttribute("fetchpriority",t.fetchpriority),i.appendChild(e),this.mainImgEl=e}if(this.container.appendChild(i),t.caption){const e=document.createElement("figcaption");e.className="lazy-image-caption",e.textContent=t.caption,this.container.appendChild(e)}if(t.schema){const e=document.createElement("script");e.type="application/ld+json",e.textContent=u(t),this.container.appendChild(e),this.schemaScriptEl=e}}loadMainImage(){if(!this.mainImgEl)return;this.state="loading";const t=new Image;this.options.srcset&&(t.srcset=this.options.srcset),this.options.sizes&&(t.sizes=this.options.sizes),t.src=this.options.src,t.onload=i=>{var r,n;this.state="loaded",this.mainImgEl&&(this.mainImgEl.src=this.options.src,this.options.srcset&&(this.mainImgEl.srcset=this.options.srcset),this.options.sizes&&(this.mainImgEl.sizes=this.options.sizes),this.mainImgEl.classList.add("is-loaded")),this.placeholderImgEl&&this.placeholderImgEl.classList.add("is-hidden"),this.overlayEl&&(this.overlayEl.innerHTML=""),(n=(r=this.options).onLoad)==null||n.call(r,i)},t.onerror=i=>{var r,n;if(this.state="error",this.overlayEl&&!this.options.disableBroken){this.overlayEl.classList.add("interactive");const e=this.options.retry&&(this.options.maxRetries===void 0||this.currentAttempt<this.options.maxRetries);if(this.overlayEl.innerHTML=`
|
|
2
|
+
<div class="lazy-image-broken-container">
|
|
3
|
+
${m}
|
|
4
|
+
<span>Failed to load image</span>
|
|
5
|
+
${e?'<button type="button" class="lazy-image-retry-button">Retry</button>':""}
|
|
6
|
+
</div>
|
|
7
|
+
`,e){const s=this.overlayEl.querySelector(".lazy-image-retry-button");s==null||s.addEventListener("click",()=>this.retry())}}(n=(r=this.options).onError)==null||n.call(r,i)}}retry(){var i,r;this.currentAttempt++,(r=(i=this.options).onRetry)==null||r.call(i,this.currentAttempt);const t=this.options.disableLoader??this.options.disabeLoader??!1;this.overlayEl&&(this.overlayEl.classList.remove("interactive"),this.overlayEl.innerHTML=t?"":`<div class="lazy-image-loader-container">${h}</div>`),this.loadMainImage()}getState(){return this.state}update(t){this.options={...this.options,...t},this.currentAttempt=0,this.destroy(),this.init()}destroy(){this.unobserve&&(this.unobserve(),this.unobserve=null),this.container.innerHTML="",this.wrapperEl=null,this.placeholderImgEl=null,this.mainImgEl=null,this.pictureEl=null,this.overlayEl=null,this.schemaScriptEl=null}}function w(a,t){return new y(a,t)}const k=typeof HTMLElement<"u"?HTMLElement:class{};class f extends k{constructor(){super();o(this,"shadow");o(this,"unobserve",null);o(this,"currentAttempt",0);o(this,"wrapperEl");o(this,"placeholderEl");o(this,"mainImgEl");o(this,"overlayEl");typeof window<"u"&&typeof this.attachShadow=="function"&&(this.shadow=this.attachShadow({mode:"open"}))}static get observedAttributes(){return["src","alt","placeholder","aspect-ratio","srcset","sizes","width","height","object-fit","object-position","root-margin","threshold","native","fetchpriority","decoding","fade-duration","blur","background-color","disable-loader","disable-broken","retry","caption","title"]}connectedCallback(){if(this.render(),this.hasAttribute("native"))this.loadMainImage();else{const r=this.getAttribute("root-margin")||"200px",n=parseFloat(this.getAttribute("threshold")||"0.01");this.unobserve=E(this,e=>{this.dispatchEvent(new CustomEvent("lazyintersect",{detail:{entry:e}})),this.loadMainImage()},{rootMargin:r,threshold:n})}}disconnectedCallback(){this.unobserve&&(this.unobserve(),this.unobserve=null)}attributeChangedCallback(i,r,n){r!==n&&this.isConnected&&this.connectedCallback()}render(){const i=this.getAttribute("src")||"",r=this.getAttribute("alt")||"",n=this.getAttribute("placeholder"),e=this.getAttribute("aspect-ratio"),s=this.getAttribute("width"),l=this.getAttribute("height"),c=this.getAttribute("object-fit")||"cover",p=this.getAttribute("object-position")||"center",z=this.getAttribute("fade-duration")||"300",M=this.getAttribute("blur")||"12",C=this.getAttribute("background-color")||"#c2c2c224",S=this.hasAttribute("disable-loader"),j=this.getAttribute("decoding")||"async",I=this.getAttribute("fetchpriority"),L=this.getAttribute("caption"),N=this.hasAttribute("native"),$=this.getAttribute("srcset"),A=this.getAttribute("sizes"),R=`
|
|
8
|
+
:host {
|
|
9
|
+
display: inline-block;
|
|
10
|
+
width: 100%;
|
|
11
|
+
position: relative;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
.wrapper {
|
|
15
|
+
position: relative;
|
|
16
|
+
width: 100%;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
background-color: ${C};
|
|
20
|
+
${e?`aspect-ratio: ${e};`:""}
|
|
21
|
+
${s?`width: ${s.endsWith("px")||s.endsWith("%")?s:s+"px"};`:""}
|
|
22
|
+
${l?`height: ${l.endsWith("px")||l.endsWith("%")?l:l+"px"};`:""}
|
|
23
|
+
}
|
|
24
|
+
.main-img {
|
|
25
|
+
display: block;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
object-fit: ${c};
|
|
29
|
+
object-position: ${p};
|
|
30
|
+
opacity: 0;
|
|
31
|
+
transition: opacity ${z}ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
32
|
+
will-change: opacity;
|
|
33
|
+
}
|
|
34
|
+
.main-img.is-loaded {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
.placeholder-img {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
left: 0;
|
|
41
|
+
width: 100%;
|
|
42
|
+
height: 100%;
|
|
43
|
+
object-fit: ${c};
|
|
44
|
+
object-position: ${p};
|
|
45
|
+
filter: blur(${M}px);
|
|
46
|
+
transform: scale(1.06);
|
|
47
|
+
transition: opacity ${z}ms ease-out;
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
.placeholder-img.is-hidden {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
visibility: hidden;
|
|
53
|
+
}
|
|
54
|
+
.overlay {
|
|
55
|
+
position: absolute;
|
|
56
|
+
inset: 0;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
z-index: 2;
|
|
62
|
+
}
|
|
63
|
+
.overlay.interactive {
|
|
64
|
+
pointer-events: auto;
|
|
65
|
+
}
|
|
66
|
+
.spinner-wrap {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
color: #629aa9;
|
|
73
|
+
}
|
|
74
|
+
.lazy-image-spinner {
|
|
75
|
+
width: 48px;
|
|
76
|
+
height: 48px;
|
|
77
|
+
animation: spin 1s linear infinite;
|
|
78
|
+
}
|
|
79
|
+
.lazy-image-spinner circle {
|
|
80
|
+
stroke: currentColor;
|
|
81
|
+
stroke-dasharray: 113.1 39.7;
|
|
82
|
+
stroke-width: 4;
|
|
83
|
+
fill: none;
|
|
84
|
+
}
|
|
85
|
+
@keyframes spin {
|
|
86
|
+
to { transform: rotate(360deg); }
|
|
87
|
+
}
|
|
88
|
+
.broken-wrap {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
padding: 16px;
|
|
95
|
+
font-family: monospace;
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
color: #4b5563;
|
|
98
|
+
text-align: center;
|
|
99
|
+
}
|
|
100
|
+
.broken-icon {
|
|
101
|
+
width: 40px;
|
|
102
|
+
height: 40px;
|
|
103
|
+
fill: currentColor;
|
|
104
|
+
opacity: 0.65;
|
|
105
|
+
}
|
|
106
|
+
.retry-btn {
|
|
107
|
+
margin-top: 4px;
|
|
108
|
+
padding: 4px 10px;
|
|
109
|
+
font-size: 11px;
|
|
110
|
+
color: #fff;
|
|
111
|
+
background-color: #629aa9;
|
|
112
|
+
border: none;
|
|
113
|
+
border-radius: 4px;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
116
|
+
.caption {
|
|
117
|
+
display: block;
|
|
118
|
+
margin-top: 6px;
|
|
119
|
+
font-size: 0.875rem;
|
|
120
|
+
color: #4b5563;
|
|
121
|
+
}
|
|
122
|
+
`;this.shadow.innerHTML=`
|
|
123
|
+
<style>${R}</style>
|
|
124
|
+
<div class="wrapper">
|
|
125
|
+
${n?`<img class="placeholder-img" src="${n}" alt="" aria-hidden="true" decoding="async" />`:""}
|
|
126
|
+
<div class="overlay">
|
|
127
|
+
${S?"":`<slot name="loader"><div class="spinner-wrap">${h}</div></slot>`}
|
|
128
|
+
</div>
|
|
129
|
+
<img class="main-img" alt="${r}" ${N?`loading="lazy" src="${i}"`:""} ${$?`srcset="${$}"`:""} ${A?`sizes="${A}"`:""} decoding="${j}" ${I?`fetchpriority="${I}"`:""} />
|
|
130
|
+
</div>
|
|
131
|
+
${L?`<figcaption class="caption"><slot name="caption">${L}</slot></figcaption>`:""}
|
|
132
|
+
`,this.wrapperEl=this.shadow.querySelector(".wrapper"),this.placeholderEl=this.shadow.querySelector(".placeholder-img"),this.mainImgEl=this.shadow.querySelector(".main-img"),this.overlayEl=this.shadow.querySelector(".overlay")}loadMainImage(){const i=this.getAttribute("src");if(!i||!this.mainImgEl)return;const r=new Image,n=this.getAttribute("srcset"),e=this.getAttribute("sizes");n&&(r.srcset=n),e&&(r.sizes=e),r.src=i,r.onload=s=>{this.mainImgEl.src=i,n&&(this.mainImgEl.srcset=n),e&&(this.mainImgEl.sizes=e),this.mainImgEl.classList.add("is-loaded"),this.placeholderEl&&this.placeholderEl.classList.add("is-hidden"),this.overlayEl&&(this.overlayEl.innerHTML=""),this.dispatchEvent(new CustomEvent("lazyload",{detail:{event:s}}))},r.onerror=s=>{const l=this.hasAttribute("disable-broken"),c=this.hasAttribute("retry");if(this.overlayEl&&!l&&(this.overlayEl.classList.add("interactive"),this.overlayEl.innerHTML=`
|
|
133
|
+
<slot name="broken">
|
|
134
|
+
<div class="broken-wrap">
|
|
135
|
+
<div class="broken-icon">${m}</div>
|
|
136
|
+
<span>Failed to load image</span>
|
|
137
|
+
${c?'<button type="button" class="retry-btn">Retry</button>':""}
|
|
138
|
+
</div>
|
|
139
|
+
</slot>
|
|
140
|
+
`,c)){const p=this.overlayEl.querySelector(".retry-btn");p==null||p.addEventListener("click",()=>this.retry())}this.dispatchEvent(new CustomEvent("lazyerror",{detail:{event:s}}))}}retry(){this.currentAttempt++,this.dispatchEvent(new CustomEvent("lazyretry",{detail:{attempt:this.currentAttempt}}));const i=this.hasAttribute("disable-loader");this.overlayEl&&(this.overlayEl.classList.remove("interactive"),this.overlayEl.innerHTML=i?"":`<slot name="loader"><div class="spinner-wrap">${h}</div></slot>`),this.loadMainImage()}}function b(a="lazy-image"){typeof window<"u"&&"customElements"in window&&(customElements.get(a)||customElements.define(a,f))}if(typeof window<"u"&&b(),b(),typeof window<"u"){const a=window;a.LazyImage=y,a.createLazyImage=w,a.createImageSchema=u,a.LazyImageElement=f}return d.BROKEN_SVG=m,d.LazyImage=y,d.LazyImageElement=f,d.SPINNER_SVG=h,d.createImageSchema=u,d.createLazyImage=w,d.defineLazyImageElement=b,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"}),d})({});
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LazyImage, createLazyImage, createImageSchema, SPINNER_SVG, BROKEN_SVG } from './vanilla/index.js';
|
|
2
|
+
import { LazyImageElement, defineLazyImageElement } from './element/index.js';
|
|
3
|
+
export { LazyImage, createLazyImage, createImageSchema, LazyImageElement, defineLazyImageElement, SPINNER_SVG, BROKEN_SVG };
|
|
4
|
+
// Auto-register <lazy-image> custom element
|
|
5
|
+
defineLazyImageElement();
|
|
6
|
+
// Attach to window for direct <script> tag usage
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
const w = window;
|
|
9
|
+
w.LazyImage = LazyImage;
|
|
10
|
+
w.createLazyImage = createLazyImage;
|
|
11
|
+
w.createImageSchema = createImageSchema;
|
|
12
|
+
w.LazyImageElement = LazyImageElement;
|
|
13
|
+
}
|