@mpxjs/webpack-plugin
Version:
mpx compile core
300 lines (299 loc) • 11.9 kB
JSX
/**
* ✔ src
* ✔ mode
* ✘ show-menu-by-longpress
* ✔ binderror
* ✔ bindload
* ✘ fade-in
* ✔ webp
* ✘ lazy-load
* ✔ bindtap
* ✔ DEFAULT_SIZE
*/
import { useEffect, useMemo, useState, useRef, forwardRef, createElement } from 'react';
import { Image as RNImage, View } from 'react-native';
import { noop } from '@mpxjs/utils';
import { SvgCssUri } from 'react-native-svg/css';
import useInnerProps, { getCustomEvent } from './getInnerListeners';
import useNodesRef from './useNodesRef';
import { SVG_REGEXP, useLayout, useTransformStyle, renderImage, extendObject } from './utils';
import Portal from './mpx-portal';
const DEFAULT_IMAGE_WIDTH = 320;
const DEFAULT_IMAGE_HEIGHT = 240;
const cropMode = [
'top',
'bottom',
'center',
'right',
'left',
'top left',
'top right',
'bottom left',
'bottom right'
];
const ModeMap = new Map([
['scaleToFill', 'stretch'],
['aspectFit', 'contain'],
['aspectFill', 'cover'],
['widthFix', 'stretch'],
['heightFix', 'stretch'],
...cropMode.map(mode => [mode, 'stretch'])
]);
const isNumber = (value) => typeof value === 'number';
const relativeCenteredSize = (viewSize, imageSize) => (viewSize - imageSize) / 2;
function noMeetCalcRule(isSvg, mode, viewWidth, viewHeight, ratio) {
const isMeetSize = viewWidth && viewHeight && ratio;
if (isSvg && !isMeetSize)
return true;
if (!isSvg && !['scaleToFill', 'aspectFit', 'aspectFill'].includes(mode) && !isMeetSize)
return true;
return false;
}
const Image = forwardRef((props, ref) => {
const { src = '', mode = 'scaleToFill', style = {}, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'enable-fast-image': enableFastImage, 'parent-width': parentWidth, 'parent-height': parentHeight, bindload, binderror } = props;
const defaultStyle = {
width: DEFAULT_IMAGE_WIDTH,
height: DEFAULT_IMAGE_HEIGHT
};
const styleObj = extendObject({}, defaultStyle, style, { overflow: 'hidden' });
const state = useRef({});
const nodeRef = useRef(null);
useNodesRef(props, ref, nodeRef, {
defaultStyle
});
const isSvg = SVG_REGEXP.test(src);
const isWidthFixMode = mode === 'widthFix';
const isHeightFixMode = mode === 'heightFix';
const isCropMode = cropMode.includes(mode);
const isLayoutMode = isWidthFixMode || isHeightFixMode || isCropMode;
const resizeMode = ModeMap.get(mode) || 'stretch';
const onLayout = ({ nativeEvent: { layout: { width, height } } }) => {
state.current.viewWidth = width;
state.current.viewHeight = height;
if (state.current.imageWidth && state.current.imageHeight && state.current.ratio) {
setViewWidth(width);
setViewHeight(height);
setRatio(state.current.ratio);
setImageWidth(state.current.imageWidth);
setImageHeight(state.current.imageHeight);
state.current = {};
setLoaded(true);
}
};
const { hasPositionFixed, hasSelfPercent, normalStyle, setWidth, setHeight } = useTransformStyle(styleObj, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
const { layoutRef, layoutStyle, layoutProps } = useLayout({
props,
hasSelfPercent,
setWidth,
setHeight,
nodeRef,
onLayout: isLayoutMode ? onLayout : noop
});
const { width, height } = normalStyle;
const [viewWidth, setViewWidth] = useState(isNumber(width) ? width : 0);
const [viewHeight, setViewHeight] = useState(isNumber(height) ? height : 0);
const [imageWidth, setImageWidth] = useState(0);
const [imageHeight, setImageHeight] = useState(0);
const [ratio, setRatio] = useState(0);
const [loaded, setLoaded] = useState(!isLayoutMode);
const fixedHeight = useMemo(() => {
const fixed = viewWidth * ratio;
return !fixed ? viewHeight : fixed;
}, [ratio, viewWidth, viewHeight]);
const fixedWidth = useMemo(() => {
if (!ratio)
return viewWidth;
const fixed = viewHeight / ratio;
return !fixed ? viewWidth : fixed;
}, [ratio, viewWidth, viewHeight]);
const modeStyle = useMemo(() => {
if (noMeetCalcRule(isSvg, mode, viewWidth, viewHeight, ratio))
return {};
switch (mode) {
case 'scaleToFill':
case 'aspectFit':
if (isSvg) {
const scale = ratio <= 1
? imageWidth >= viewWidth ? viewWidth / imageWidth : imageWidth / viewWidth
: imageHeight >= viewHeight ? viewHeight / imageHeight : imageHeight / viewHeight;
return {
transform: [
{ scale },
ratio <= 1 ? { translateY: -(imageHeight * scale - viewHeight) / 2 / scale } : { translateX: -(imageWidth * scale - viewWidth) / 2 / scale }
]
};
}
return {};
case 'aspectFill':
if (isSvg) {
const scale = ratio >= 1
? imageWidth >= viewWidth ? viewWidth / imageWidth : imageWidth / viewWidth
: imageHeight >= viewHeight ? viewHeight / imageHeight : imageHeight / viewHeight;
return {
transform: [
{ scale },
ratio >= 1 ? { translateY: -(imageHeight * scale - viewHeight) / 2 / scale } : { translateX: -(imageWidth * scale - viewWidth) / 2 / scale }
]
};
}
return {};
case 'widthFix':
case 'heightFix':
if (isSvg) {
const scale = ratio >= 1
? imageWidth >= fixedWidth ? fixedWidth / imageWidth : imageWidth / fixedWidth
: imageHeight >= fixedHeight ? fixedHeight / imageHeight : imageHeight / fixedHeight;
return {
transform: [{ scale }]
};
}
return {};
case 'top':
return {
transform: [
{ translateX: relativeCenteredSize(viewWidth, imageWidth) }
]
};
case 'bottom':
return {
transform: [
{ translateY: viewHeight - imageHeight },
{ translateX: relativeCenteredSize(viewWidth, imageWidth) }
]
};
case 'center':
return {
transform: [
{ translateY: relativeCenteredSize(viewHeight, imageHeight) },
{ translateX: relativeCenteredSize(viewWidth, imageWidth) }
]
};
case 'left':
return {
transform: [
{ translateY: relativeCenteredSize(viewHeight, imageHeight) }
]
};
case 'right':
return {
transform: [
{ translateY: relativeCenteredSize(viewHeight, imageHeight) },
{ translateX: viewWidth - imageWidth }
]
};
case 'top left':
return {};
case 'top right':
return {
transform: [
{ translateX: viewWidth - imageWidth }
]
};
case 'bottom left':
return {
transform: [
{ translateY: viewHeight - imageHeight }
]
};
case 'bottom right':
return {
transform: [
{ translateY: viewHeight - imageHeight },
{ translateX: viewWidth - imageWidth }
]
};
default:
return {};
}
}, [isSvg, mode, viewWidth, viewHeight, imageWidth, imageHeight, ratio, fixedWidth, fixedHeight]);
const onSvgLoad = (evt) => {
const { width, height } = evt.nativeEvent.layout;
setRatio(!width ? 0 : height / width);
setImageWidth(width);
setImageHeight(height);
bindload && bindload(getCustomEvent('load', evt, {
detail: { width, height },
layoutRef
}, props));
};
const onSvgError = (evt) => {
binderror(getCustomEvent('error', evt, {
detail: { errMsg: evt?.message },
layoutRef
}, props));
};
const onImageLoad = (evt) => {
evt.persist();
RNImage.getSize(src, (width, height) => {
bindload(getCustomEvent('load', evt, {
detail: { width, height },
layoutRef
}, props));
});
};
const onImageError = (evt) => {
binderror(getCustomEvent('error', evt, {
detail: { errMsg: evt.nativeEvent.error },
layoutRef
}, props));
};
useEffect(() => {
if (!isSvg && isLayoutMode) {
RNImage.getSize(src, (width, height) => {
state.current.imageWidth = width;
state.current.imageHeight = height;
state.current.ratio = !width ? 0 : height / width;
if (isWidthFixMode
? state.current.viewWidth
: isHeightFixMode
? state.current.viewHeight
: state.current.viewWidth && state.current.viewHeight) {
state.current.viewWidth && setViewWidth(state.current.viewWidth);
state.current.viewHeight && setViewHeight(state.current.viewHeight);
setRatio(!width ? 0 : height / width);
setImageWidth(width);
setImageHeight(height);
state.current = {};
setLoaded(true);
}
}, () => {
setLoaded(true);
});
}
}, [src, isSvg, isLayoutMode]);
const innerProps = useInnerProps(extendObject({}, props, layoutProps, {
ref: nodeRef,
style: extendObject({}, normalStyle, layoutStyle, isHeightFixMode ? { width: fixedWidth } : {}, isWidthFixMode ? { height: fixedHeight } : {})
}), [
'src',
'mode',
'svg'
], {
layoutRef
});
const SvgImage = createElement(View, innerProps, createElement(SvgCssUri, {
uri: src,
onLayout: onSvgLoad,
onError: binderror && onSvgError,
style: extendObject({ transformOrigin: 'left top' }, modeStyle)
}));
const BaseImage = renderImage(extendObject({
source: { uri: src },
resizeMode: resizeMode,
onLoad: bindload && onImageLoad,
onError: binderror && onImageError,
style: extendObject({
transformOrigin: 'left top',
width: isCropMode ? imageWidth : '100%',
height: isCropMode ? imageHeight : '100%'
}, isCropMode ? modeStyle : {})
}, isLayoutMode ? {} : innerProps), enableFastImage);
const LayoutImage = createElement(View, innerProps, loaded && BaseImage);
const finalComponent = isSvg ? SvgImage : isLayoutMode ? LayoutImage : BaseImage;
if (hasPositionFixed) {
return createElement(Portal, null, finalComponent);
}
return finalComponent;
});
Image.displayName = 'mpx-image';
export default Image;