react-native-fast-image
Version:
FastImage, performant React Native image component.
531 lines (530 loc) • 18.7 kB
JavaScript
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let react = require("react");
react = __toESM(react);
let react_native = require("react-native");
let react_native_web_dist_modules_AssetRegistry = require("react-native-web/dist/modules/AssetRegistry");
//#region src/constants.ts
const resizeMode = {
contain: "contain",
cover: "cover",
stretch: "stretch",
center: "center",
repeat: "repeat"
};
const priority = {
low: "low",
normal: "normal",
high: "high"
};
const cacheControl = {
immutable: "immutable",
web: "web",
cacheOnly: "cacheOnly"
};
//#endregion
//#region src/objectFit.ts
function fromStyle(style, key) {
if (Array.isArray(style)) {
for (let i = style.length - 1; i >= 0; i--) {
const found = fromStyle(style[i], key);
if (found !== void 0) return found;
}
return;
}
if (typeof style === "number") {
const flattened = react_native.StyleSheet.flatten(style);
return flattened ? flattened[key] : void 0;
}
return style && typeof style === "object" ? style[key] : void 0;
}
const FITS = {
fill: true,
contain: true,
cover: true,
none: true,
"scale-down": true
};
function resolveObjectFit(objectFit, style) {
const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
return fit && FITS[fit] === true ? fit : "cover";
}
//#endregion
//#region src/warnings.ts
const warned = /* @__PURE__ */ new Set();
function warnOnce(key, message, level) {
if (!warned.has(key)) {
warned.add(key);
console[level](message);
}
}
function warnIfChildren(children) {
if (react.Children.toArray(children).length > 0) warnOnce("children", "react-native-fast-image: FastImage doesn't render children (since 10.0): use FastImageBackground for content over an image.", "error");
}
//#endregion
//#region src/objectPosition.ts
const START = {
fraction: 0,
offset: 0
};
const CENTER = {
fraction: .5,
offset: 0
};
const END = {
fraction: 1,
offset: 0
};
/** The default, `50% 50%`. */
const CENTERED = {
x: CENTER,
y: CENTER
};
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
function length(text) {
const lower = text.toLowerCase();
if (lower.endsWith("%")) {
const number = lower.slice(0, -1);
return NUMBER.test(number) ? {
fraction: Number(number) / 100,
offset: 0
} : null;
}
if (lower.endsWith("px")) {
const number = lower.slice(0, -2);
return NUMBER.test(number) ? {
fraction: 0,
offset: Number(number)
} : null;
}
return NUMBER.test(lower) && Number(lower) === 0 ? START : null;
}
function token(text) {
const lower = text.toLowerCase();
if (lower === "left" || lower === "right" || lower === "top" || lower === "bottom" || lower === "center") return { keyword: lower };
const axis = length(text);
return axis ? { length: axis } : null;
}
const X = {
left: START,
center: CENTER,
right: END
};
const Y = {
top: START,
center: CENTER,
bottom: END
};
const x = (t) => "length" in t ? t.length : X[t.keyword];
const y = (t) => "length" in t ? t.length : Y[t.keyword];
function fromEdge(edge, axis) {
return edge === "left" || edge === "top" ? axis : {
fraction: 1 - axis.fraction,
offset: -axis.offset || 0
};
}
function parse(text) {
const tokens = [];
for (const part of text.trim().split(/\s+/)) {
const t = token(part);
if (!t) return null;
tokens.push(t);
}
if (tokens.length === 1) {
const [a] = tokens;
if ("keyword" in a && (a.keyword === "top" || a.keyword === "bottom")) return {
x: CENTER,
y: Y[a.keyword]
};
return {
x: x(a),
y: CENTER
};
}
if (tokens.length === 2) {
const [a, b] = tokens;
const ax = x(a);
const by = y(b);
if (ax && by) return {
x: ax,
y: by
};
if ("keyword" in a && "keyword" in b) {
const bx = X[b.keyword];
const ay = Y[a.keyword];
if (bx && ay) return {
x: bx,
y: ay
};
}
return null;
}
if (tokens.length === 4) {
const [a, b, c, d] = tokens;
if (!("keyword" in a && "length" in b && "keyword" in c && "length" in d)) return null;
const horizontal = (k) => k === "left" || k === "right";
const vertical = (k) => k === "top" || k === "bottom";
if (horizontal(a.keyword) && vertical(c.keyword)) return {
x: fromEdge(a.keyword, b.length),
y: fromEdge(c.keyword, d.length)
};
if (vertical(a.keyword) && horizontal(c.keyword)) return {
x: fromEdge(c.keyword, d.length),
y: fromEdge(a.keyword, b.length)
};
}
return null;
}
const parsed = /* @__PURE__ */ new Map();
function objectLength(value) {
if (typeof value === "number") return Number.isFinite(value) ? {
fraction: 0,
offset: value
} : null;
return typeof value === "string" && value.endsWith("%") ? length(value) : null;
}
/**
* The position for objectPosition, or null if it isn't valid. A string is
* CSS's syntax; an object has one of `top` and `bottom` and one of `left`
* and `right`, each in points or a percentage, and a missing axis is
* centered. Unset is centered.
*/
function resolveObjectPosition(position) {
if (position == null) return CENTERED;
if (typeof position === "string") {
let result = parsed.get(position);
if (result === void 0) {
result = parse(position);
if (parsed.size < 100) parsed.set(position, result);
}
return result;
}
if (typeof position !== "object") return null;
const edges = position;
const axis = (start, end) => {
const hasStart = edges[start] != null;
const hasEnd = edges[end] != null;
if (hasStart && hasEnd) return null;
if (!hasStart && !hasEnd) return CENTER;
const edge = hasStart ? start : end;
const value = objectLength(edges[edge]);
return value && fromEdge(edge, value);
};
const resolvedX = axis("left", "right");
const resolvedY = axis("top", "bottom");
return resolvedX && resolvedY ? {
x: resolvedX,
y: resolvedY
} : null;
}
const round = (n) => Math.round(n * 1e6) / 1e6;
/**
* The position for objectPosition, or centered if it isn't valid (as CSS
* ignores it), with a warning in development.
*/
function objectPositionOrCentered(position) {
const resolved = resolveObjectPosition(position);
if (resolved) return resolved;
if (typeof __DEV__ !== "undefined" && __DEV__) {
const value = JSON.stringify(position);
warnOnce(`objectPosition ${value}`, `react-native-fast-image: objectPosition ${value} isn't a position, so the image is centered. Use CSS's syntax with px or % (e.g. 'right 10px bottom 20%'), or { top | bottom, left | right } in points or percentages.`, "warn");
}
return CENTERED;
}
/** The position as CSS (for the web version's object-position). */
function cssPosition({ x, y }) {
const axis = ({ fraction, offset }) => {
const percent = round(fraction * 100);
const px = round(offset);
if (px === 0) return `${percent}%`;
if (percent === 0) return `${px}px`;
return `calc(${percent}% ${px < 0 ? "-" : "+"} ${Math.abs(px)}px)`;
};
return `${axis(x)} ${axis(y)}`;
}
//#endregion
//#region src/index.web.tsx
var _useId;
const notSupported = {
ok: false,
error: "Not supported on the web"
};
let ids = 0;
const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
const label = ariaLabel || accessibilityLabel || void 0;
const several = Array.isArray(source) && source.length > 1 ? source : void 0;
const sized = several ? sizedSources(several) : void 0;
const src = sized ? sized.src : resolveUri(source);
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
const defaultUri = resolveUri(defaultSource);
const [result, setResult] = (0, react.useState)();
const ok = result && result.key === key ? result.ok : void 0;
const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
const fit = resolveObjectFit(objectFit, style);
const position = cssPosition(objectPositionOrCentered(objectPosition));
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
const showDefault = !!defaultUri && ok !== true;
useClientLayoutEffect(() => {
if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
}, [key]);
return /* @__PURE__ */ react.default.createElement(react_native.View, {
...viewProps,
"aria-label": label,
style: [styles.container, style],
onLayout,
pointerEvents,
ref: forwardedRef
}, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
key: `default ${defaultUri}`,
src: defaultUri,
fit,
position,
filter,
alt: key ? "" : label !== null && label !== void 0 ? label : ""
}) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
key: several ? "sizes" : key,
src,
srcSet,
lazy: !!several,
fit,
position,
filter,
hidden: ok === false,
alt: label !== null && label !== void 0 ? label : "",
onLoad: (image) => {
const { width, height } = loadedSize(image, sized);
setResult({
key,
ok: true
});
onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
width,
height
} });
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
ok: true,
width,
height
});
},
onError: (uri) => {
const error = `Failed to load resource ${uri}`;
setResult({
key,
ok: false
});
onError === null || onError === void 0 || onError({ nativeEvent: { error } });
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
ok: false,
error
});
}
}) : null, tintColor ? /* @__PURE__ */ react.default.createElement(TintFilter, {
id: tintId,
color: tintColor
}) : null);
}
function resolveUri(source) {
let uri;
if (typeof source === "number") {
var _asset$scales$;
const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
if (!asset) return void 0;
const scale = asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
const suffix = scale !== 1 ? `@${scale}x` : "";
uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
} else if (typeof source === "string") uri = source;
else if (Array.isArray(source)) return resolveUri(source[0]);
else if (source && typeof source.uri === "string") uri = source.uri;
const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
}
function sizedSources(sources) {
var _largest$uri, _sources$find;
const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
var _source$width, _source$scale;
return {
uri: source.uri,
width: Math.round(((_source$width = source.width) !== null && _source$width !== void 0 ? _source$width : 0) * ((_source$scale = source.scale) !== null && _source$scale !== void 0 ? _source$scale : 1))
};
});
const largest = widths.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
return {
src: (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri,
srcSet: widths.length > 0 ? widths.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0,
widths
};
}
function loadedSize(image, sized) {
var _picked$width;
const picked = (sized === null || sized === void 0 ? void 0 : sized.srcSet) ? sized.widths.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
return {
width: (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth,
height: picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight
};
}
const fill = {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%"
};
function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad, onError }) {
const image = (0, react.useRef)(null);
const mounted = (0, react.useRef)(true);
const handled = (0, react.useRef)(void 0);
const loaded = (target) => {
var _handled$current, _target$decode;
const file = target.currentSrc || src;
const sources = srcSet !== null && srcSet !== void 0 ? srcSet : src;
if (!mounted.current || ((_handled$current = handled.current) === null || _handled$current === void 0 ? void 0 : _handled$current.sources) === sources && handled.current.file === file) return;
handled.current = {
sources,
file
};
const done = () => {
if (mounted.current) onLoad === null || onLoad === void 0 || onLoad(target);
};
Promise.resolve((_target$decode = target.decode) === null || _target$decode === void 0 ? void 0 : _target$decode.call(target)).then(done, done);
};
(0, react.useEffect)(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
useClientLayoutEffect(() => {
var _target$decode2;
const target = image.current;
if (!(target === null || target === void 0 ? void 0 : target.complete)) return;
if (target.naturalWidth > 0) return loaded(target);
Promise.resolve((_target$decode2 = target.decode) === null || _target$decode2 === void 0 ? void 0 : _target$decode2.call(target)).then(() => loaded(target), () => {
if (mounted.current) onError === null || onError === void 0 || onError(target.currentSrc || src);
});
}, []);
return /* @__PURE__ */ react.default.createElement("img", {
ref: image,
src,
srcSet,
sizes: srcSet ? "auto, 100vw" : void 0,
loading: lazy ? "lazy" : void 0,
alt,
draggable: false,
style: {
...fill,
objectFit: fit,
objectPosition: position,
filter,
opacity: hidden ? 0 : void 0
},
onLoad: (event) => loaded(event.currentTarget),
onError: (event) => {
const target = event.currentTarget;
if (mounted.current) onError === null || onError === void 0 || onError((target === null || target === void 0 ? void 0 : target.currentSrc) || src);
}
});
}
function TintFilter({ id, color }) {
return /* @__PURE__ */ react.default.createElement("svg", { style: tintStyle }, /* @__PURE__ */ react.default.createElement("defs", null, /* @__PURE__ */ react.default.createElement("filter", { id }, /* @__PURE__ */ react.default.createElement("feFlood", {
floodColor: String(color),
key: String(color)
}), /* @__PURE__ */ react.default.createElement("feComposite", {
in2: "SourceAlpha",
operator: "in"
}))));
}
const tintStyle = {
position: "absolute",
width: 0,
height: 0,
visibility: "hidden"
};
const FastImageMemo = (0, react.memo)(FastImageBase);
const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
forwardedRef: ref,
...props
}));
FastImageComponent.displayName = "FastImage";
function getSize(uri) {
return new Promise((resolve) => {
react_native.Image.getSize(uri, (width, height) => resolve({
width,
height
}), () => resolve({
width: 0,
height: 0
}));
});
}
const FastImage = FastImageComponent;
FastImage.cacheControl = cacheControl;
FastImage.priority = priority;
FastImage.resizeMode = resizeMode;
FastImage.preload = (sources) => Promise.all(sources.map((source) => {
const uri = source ? source.uri : void 0;
if (typeof uri !== "string" || !uri) return Promise.resolve({
ok: false,
error: "Invalid source: no uri",
uri
});
return react_native.Image.prefetch(uri).then(() => getSize(uri).then((size) => ({
ok: true,
uri,
...size
})), () => ({
ok: false,
error: "Failed to load the image",
uri
}));
}));
FastImage.clearMemoryCache = () => Promise.resolve(notSupported);
FastImage.clearDiskCache = () => Promise.resolve(notSupported);
FastImage.configureCache = () => Promise.resolve({});
FastImage.setDefaults = () => {};
FastImage.writeToCache = () => Promise.resolve(notSupported);
FastImage.getCachePath = () => Promise.resolve(notSupported);
const styles = react_native.StyleSheet.create({ container: { overflow: "hidden" } });
const FastImageWithRef = FastImageComponent;
const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
style,
ref
}, /* @__PURE__ */ react.default.createElement(FastImageWithRef, {
...props,
style: [react_native.StyleSheet.absoluteFill, imageStyle],
ref: imageRef
}), children));
FastImageBackground.displayName = "FastImageBackground";
//#endregion
exports.FastImageBackground = FastImageBackground;
exports.default = FastImage;