Files
bknd/packages/plasmic/components/Image.tsx
2024-11-16 12:01:47 +01:00

244 lines
5.9 KiB
TypeScript

"use client";
import type { CodeComponentMeta } from "@plasmicapp/host";
//import { PlasmicCanvasContext } from "@plasmicapp/loader-react";
import { useContext, useEffect, useRef, useState } from "react";
type PlasmicImageProps = {
asset: {
aspectRatio?: any;
dataUri: string;
name: string;
type: string;
uid: number;
uuid: string;
width: number;
height: number;
};
uid: number;
};
type ImageProps = {
className?: string;
src?: string | PlasmicImageProps;
alt?: string;
width?: number | string;
height?: number | string;
ratio?: number;
backgroundColor?: string;
forceLoad?: boolean;
transformations?: string;
transitionSpeed?: number;
loadTreshold?: number;
};
function numeric(value: number | string): number {
return typeof value === "number" ? value : Number.parseFloat(value);
}
function getDimensionDefaults(
width: number | string | undefined,
height: number | string | undefined,
ratio: number | undefined,
) {
let _width = width;
let _height = height;
let _ratio = ratio;
if (_width && ratio) {
_height = (1 / ratio) * numeric(_width);
} else if (_height && ratio) {
_width = ratio * numeric(_height);
}
if (_width && _height && !_ratio) {
_ratio = numeric(_width) / numeric(_height);
}
return { width: _width, height: _height, ratio: _ratio };
}
function getPlaceholderStyle(
width: number | string | undefined,
height: number | string | undefined,
ratio: number | undefined,
) {
let paddingBottom = 0;
if (width && height) {
paddingBottom = (1 / (numeric(width) / numeric(height))) * 100;
//paddingBottom = `${numeric(width)}px / ${numeric(height)}px * 100%}`;
//
} else if (ratio) {
paddingBottom = (1 / ratio) * 100;
}
return {
paddingBottom: paddingBottom + "%",
};
}
export const Image: React.FC<ImageProps> = ({
className,
src,
alt = "",
width,
height,
ratio,
backgroundColor = "rgba(225, 225, 225, 0.2)",
forceLoad = false,
transformations = "",
transitionSpeed = 200,
loadTreshold = 0.1,
...rest
}) => {
const inEditor = false; // !!useContext(PlasmicCanvasContext);
const [loaded, setLoaded] = useState(false);
const [isInView, setIsInView] = useState(inEditor ?? forceLoad);
const [transitioned, setTransitioned] = useState(forceLoad);
const imgRef = useRef<any>(null);
if (src) {
if (typeof src === "object") {
src = src.asset.dataUri;
}
if (/cloudinary/.test(src)) {
if (transformations) {
src = src.replace("/upload", "/upload/" + transformations);
}
}
}
//console.log("after:src", src);
useEffect(() => {
if (forceLoad) {
setIsInView(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
});
},
{ threshold: loadTreshold },
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
observer.disconnect();
};
}, [forceLoad]);
const onLoad = () => {
setTimeout(() => {
setLoaded(true);
}, 0);
setTimeout(() => {
setTransitioned(true);
}, transitionSpeed);
};
const {
width: _width,
height: _height,
ratio: _ratio,
} = getDimensionDefaults(width, height, ratio);
const imgStyle: any = {
objectFit: "cover",
transition: `opacity ${transitionSpeed}ms linear`,
position: "relative",
maxWidth: "100%",
maxHeight: "100%",
width: _width || "100%",
height: "auto",
//height: _height || "auto",
//height: !transitioned ? _height || "auto" : "auto",
opacity: forceLoad || loaded ? 1 : 0,
};
const placeholderStyle: any = {
position: "absolute",
maxWidth: "100%",
maxHeight: "100%",
backgroundColor,
width: _width || "100%",
height: 0,
//height: transitioned ? "auto" : 0,
...getPlaceholderStyle(_width, _height, _ratio),
};
const wrapperStyle: any = {
position: "relative",
width: _width,
...getPlaceholderStyle(_width, _height, _ratio),
height: 0,
margin: 0,
lineHeight: 0,
//height: _height,
maxWidth: "100%",
maxHeight: "100%",
};
if (loaded) {
wrapperStyle.height = "auto";
wrapperStyle.paddingBottom = 0;
}
if (!src) return <div className={className} style={wrapperStyle} ref={imgRef} />;
return (
<div className={className} style={wrapperStyle} ref={imgRef}>
<div style={placeholderStyle} />
{isInView && (
<img
src={src}
alt={alt}
onLoad={onLoad}
style={imgStyle}
width={_width}
height={_height}
{...rest}
/>
)}
</div>
);
};
export const ImageMeta: CodeComponentMeta<React.ComponentType<ImageProps>> = {
name: "ImageLazy",
importPath: import.meta.dir,
props: {
src: {
type: "imageUrl",
displayName: "Image",
},
alt: "string",
width: "number",
height: "number",
ratio: "number",
forceLoad: "boolean",
transformations: "string",
//backgroundColor: "color",
transitionSpeed: {
type: "number",
helpText: "How fast image should fade in. Default is 200 (ms).",
},
loadTreshold: {
type: "number",
displayName: "Treshold",
//defaultValue: 0.1,
helpText:
"Number between 0 and 1. Default is 0.1. Determines how much of the image must be in viewport before it gets loaded",
},
},
};