UNPKG

react-native-fast-image

Version:
531 lines (530 loc) • 18.7 kB
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;