|
1 | 1 | import type { JSX } from "solid-js"; |
2 | | -import { ssr as ssrHtml, isServer } from "solid-js/web"; |
3 | | -import type { StartImageSource, StartImageTransformer, StartImageVariant } from "./types"; |
| 2 | +import { createMemo, createSignal, For, Show } from "solid-js"; |
| 3 | +import { ClientOnly } from "./client-only.tsx"; |
| 4 | +import { createLazyRender } from "./create-lazy-render.ts"; |
| 5 | +import { |
| 6 | + createImageVariants, |
| 7 | + mergeImageVariantsByType, |
| 8 | + mergeImageVariantsToSrcSet, |
| 9 | +} from "./transformer.ts"; |
| 10 | +import type { StartImageSource, StartImageTransformer, StartImageVariant } from "./types.ts"; |
| 11 | +import { getAspectRatioBoxStyle } from "./utils.ts"; |
4 | 12 |
|
5 | | -export interface ImageProps<T> { |
6 | | - src: { |
7 | | - src: StartImageSource<T> |
8 | | - transformer?: StartImageTransformer<T> |
9 | | - } |
| 13 | +import "./styles.css"; |
| 14 | + |
| 15 | +export interface StartImageProps<T> { |
| 16 | + src: StartImageSource<T>; |
10 | 17 | alt: string; |
11 | | - fallback?: (visible: () => boolean, onLoad: () => void) => JSX.Element; |
12 | | -} |
| 18 | + transformer?: StartImageTransformer<T>; |
13 | 19 |
|
14 | | -function mergeImageVariantsByType(variants: StartImageVariant[]) { |
15 | | - const map = new Map<string, StartImageVariant[]>(); |
16 | | - for (const variant of variants) { |
17 | | - const arr = map.get(variant.type) || []; |
18 | | - arr.push(variant); |
19 | | - map.set(variant.type, arr); |
20 | | - } |
21 | | - return map; |
| 20 | + onLoad?: () => void; |
| 21 | + fallback: (visible: () => boolean, onLoad: () => void) => JSX.Element; |
| 22 | + |
| 23 | + crossOrigin?: JSX.HTMLCrossorigin | undefined; |
| 24 | + fetchPriority?: "high" | "low" | "auto" | undefined; |
| 25 | + decoding?: "sync" | "async" | "auto" | undefined; |
22 | 26 | } |
23 | 27 |
|
24 | | -function mergeImageVariantsToSrcSet(variants: StartImageVariant[]) { |
25 | | - return variants.map(v => `${v.path} ${v.width}w`).join(","); |
| 28 | +interface StartImageSourcesProps<T> extends StartImageProps<T> { |
| 29 | + variants: StartImageVariant[]; |
26 | 30 | } |
27 | 31 |
|
28 | | -export function Image<T>(props: ImageProps<T>): JSX.Element { |
29 | | - if (isServer) { |
30 | | - const variants = props.src.transformer |
31 | | - ? props.src.transformer.transform(props.src.src) |
32 | | - : []; |
33 | | - const variantArray = Array.isArray(variants) ? variants : [variants]; |
34 | | - |
35 | | - let html = `<div data-start-image="container">`; |
36 | | - html += `<div data-start-image="aspect-ratio" style="position:relative;padding-top:${(props.src.src.height * 100) / props.src.src.width}%;width:100%;height:0;overflow:hidden;">`; |
37 | | - html += `<picture data-start-picture="picture">`; |
38 | | - |
39 | | - if (variantArray.length > 0) { |
40 | | - const merged = mergeImageVariantsByType(variantArray); |
41 | | - for (const [type, vars] of merged) { |
42 | | - const srcset = mergeImageVariantsToSrcSet(vars); |
43 | | - html += `<source type="${type}" srcset="${srcset}"/>`; |
44 | | - } |
| 32 | +function StartImageSources<T>(props: StartImageSourcesProps<T>): JSX.Element { |
| 33 | + const mergedVariants = createMemo(() => { |
| 34 | + const types = mergeImageVariantsByType(props.variants); |
| 35 | + |
| 36 | + const values: [type: string, srcset: string][] = []; |
| 37 | + |
| 38 | + for (const [key, variants] of types) { |
| 39 | + values.push([key, mergeImageVariantsToSrcSet(variants)]); |
45 | 40 | } |
46 | | - |
47 | | - html += `<img data-start-image="image" src="${props.src.src.source}" alt="${props.alt}" style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;"/>`; |
48 | | - html += `</picture></div></div>`; |
49 | | - |
50 | | - return ssrHtml(html) as unknown as JSX.Element; |
| 41 | + |
| 42 | + return values; |
| 43 | + }); |
| 44 | + |
| 45 | + return ( |
| 46 | + <For each={mergedVariants()}>{([type, srcset]) => <source type={type} srcset={srcset} />}</For> |
| 47 | + ); |
| 48 | +} |
| 49 | + |
| 50 | +export function StartImage<T>(props: StartImageProps<T>): JSX.Element { |
| 51 | + const [showPlaceholder, setShowPlaceholder] = createSignal(true); |
| 52 | + const laze = createLazyRender<HTMLDivElement>(); |
| 53 | + const [defer, setDefer] = createSignal(true); |
| 54 | + |
| 55 | + function onPlaceholderLoad() { |
| 56 | + setDefer(false); |
51 | 57 | } |
52 | | - |
| 58 | + |
| 59 | + const width = createMemo(() => props.src.width); |
| 60 | + const height = createMemo(() => props.src.height); |
| 61 | + |
53 | 62 | return ( |
54 | | - <div data-start-image="container"> |
55 | | - <div |
56 | | - data-start-image="aspect-ratio" |
57 | | - style={{ |
58 | | - position: "relative", |
59 | | - "padding-top": `${(props.src.src.height * 100) / props.src.src.width}%`, |
60 | | - width: "100%", |
61 | | - height: "0", |
62 | | - overflow: "hidden", |
63 | | - }} |
| 63 | + <div ref={laze.ref} data-start-image="container"> |
| 64 | + <div |
| 65 | + data-start-image="aspect-ratio" |
| 66 | + style={getAspectRatioBoxStyle({ |
| 67 | + width: width(), |
| 68 | + height: height(), |
| 69 | + })} |
64 | 70 | > |
65 | | - <picture data-start-picture="picture"> |
66 | | - {props.src.transformer && (() => { |
67 | | - const variants = props.src.transformer!.transform(props.src.src); |
68 | | - const variantArray = Array.isArray(variants) ? variants : [variants]; |
69 | | - const merged = mergeImageVariantsByType(variantArray); |
70 | | - return Array.from(merged).map(([type, vars]) => ( |
71 | | - <source |
72 | | - type={type} |
73 | | - srcset={mergeImageVariantsToSrcSet(vars)} |
| 71 | + <picture data-start-image="picture"> |
| 72 | + <Show when={props.transformer} fallback={<source src={props.src.source} />}> |
| 73 | + {cb => <StartImageSources variants={createImageVariants(props.src, cb())} {...props} />} |
| 74 | + </Show> |
| 75 | + <ClientOnly |
| 76 | + fallback={ |
| 77 | + <img |
| 78 | + data-start-image="image" |
| 79 | + alt={props.alt} |
| 80 | + crossOrigin={props.crossOrigin} |
| 81 | + fetchpriority={props.fetchPriority} |
| 82 | + decoding={props.decoding} |
74 | 83 | /> |
75 | | - )); |
76 | | - })()} |
77 | | - <img |
78 | | - data-start-image="image" |
79 | | - src={props.src.src.source} |
80 | | - alt={props.alt} |
81 | | - style={{ |
82 | | - position: "absolute", |
83 | | - top: "0", |
84 | | - left: "0", |
85 | | - width: "100%", |
86 | | - height: "100%", |
87 | | - "object-fit": "contain", |
88 | | - }} |
89 | | - /> |
| 84 | + } |
| 85 | + > |
| 86 | + <Show when={laze.visible}> |
| 87 | + <img |
| 88 | + data-start-image="image" |
| 89 | + // src={getEmptyImageURL({ |
| 90 | + // width: width(), |
| 91 | + // height: height(), |
| 92 | + // })} |
| 93 | + alt={props.alt} |
| 94 | + onLoad={() => { |
| 95 | + if (!defer()) { |
| 96 | + setShowPlaceholder(false); |
| 97 | + props.onLoad?.(); |
| 98 | + } |
| 99 | + }} |
| 100 | + style={{ |
| 101 | + opacity: showPlaceholder() ? 0 : 1, |
| 102 | + }} |
| 103 | + crossOrigin={props.crossOrigin} |
| 104 | + fetchpriority={props.fetchPriority} |
| 105 | + decoding={props.decoding} |
| 106 | + /> |
| 107 | + </Show> |
| 108 | + </ClientOnly> |
90 | 109 | </picture> |
91 | 110 | </div> |
| 111 | + <div data-start-image="blocker"> |
| 112 | + <ClientOnly> |
| 113 | + <Show when={laze.visible}>{props.fallback(showPlaceholder, onPlaceholderLoad)}</Show> |
| 114 | + </ClientOnly> |
| 115 | + </div> |
92 | 116 | </div> |
93 | 117 | ); |
94 | 118 | } |
0 commit comments