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 CHANGED
@@ -1,58 +1,263 @@
1
- # lazy-image-loader
1
+ # lazy-svelte-image
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/lazy-svelte-image.svg)](https://www.npmjs.com/package/lazy-svelte-image)
4
+ [![license](https://img.shields.io/npm/l/lazy-svelte-image.svg)](https://github.com/rishabharidas/lazy-svelte-image/blob/main/LICENSE)
5
+ [![svelte](https://img.shields.io/badge/svelte-5%20%7C%204%20%7C%203-FF3E00.svg)](https://svelte.dev)
6
+ [![vulnerabilities](https://img.shields.io/badge/vulnerabilities-0-brightgreen.svg)](https://github.com/rishabharidas/lazy-svelte-image)
7
+ [![Universal JS](https://img.shields.io/badge/Vanilla%20JS%20%26%20Web%20Component-supported-blue.svg)](https://github.com/rishabharidas/lazy-svelte-image)
3
8
 
4
- A custom Image component/tag like component with customizations, loader ...
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
- ## Installing the Package
11
+ ---
7
12
 
8
- If you're seeing this, you've probably already done this step. Congrats!
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 i lazy-svelte-image
32
+ npm install lazy-svelte-image
12
33
  ```
13
34
 
14
- ## Usage
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
- Once you've installed you can import the package and use just like ```<img>``` . an exaple is given below
37
+ ---
17
38
 
18
- ```bash
19
- import { Image } from 'lazy-svelte-image'
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
- <Image src="" alt="" />
102
+ Works in **React**, **Vue**, **Angular**, **Solid**, or plain HTML without requiring any Svelte runtime!
23
103
 
24
- # or
104
+ ```html
105
+ <!-- Import the web component -->
106
+ <script type="module">
107
+ import 'lazy-svelte-image/element';
108
+ </script>
25
109
 
26
- <Image> <!-- slot--> </Image>
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
- ## Customization
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
- Currenlty you can provide custom loader and broken image icon as slot. use slot like how you use it in svelte.
240
+ ### Events
33
241
 
34
- ### Props
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
- - <code>disableLoader</code> - disable all loaders
37
- - <code>disableBroken</code> - disable all broken view
38
- - <code>backgroundColor</code> - set custom background color for default broken/loader
249
+ ---
39
250
 
40
- ### Slots
251
+ ## 🔄 Backward Compatibility with v1.x
41
252
 
42
- #### For custom broken Images/Icons
253
+ This version is **100% backward compatible** with `lazy-svelte-image` v1.1.2:
43
254
 
44
- ```bash
45
- <span slot="boken">
46
- <!-- provide code here -->
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
- ```bash
52
- <span slot="loader">
53
- <!-- provide loader code here -->
54
- </span>
55
- ```
259
+ ---
56
260
 
261
+ ## 📄 License
57
262
 
58
- ##### Looking forward for feature requests and usages 😃..
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})({});
@@ -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
+ }