363 lines
9.9 KiB
JavaScript
363 lines
9.9 KiB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
import CSSMotion from '@rc-component/motion';
|
|
import Portal from '@rc-component/portal';
|
|
import { useEvent } from '@rc-component/util';
|
|
import { useLockFocus } from "@rc-component/util/es/Dom/focus";
|
|
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
|
import KeyCode from "@rc-component/util/es/KeyCode";
|
|
import { clsx } from 'clsx';
|
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
import { PreviewGroupContext } from "../context";
|
|
import useImageTransform from "../hooks/useImageTransform";
|
|
import useMouseEvent from "../hooks/useMouseEvent";
|
|
import useStatus from "../hooks/useStatus";
|
|
import useTouchEvent from "../hooks/useTouchEvent";
|
|
import { BASE_SCALE_RATIO } from "../previewConfig";
|
|
import CloseBtn from "./CloseBtn";
|
|
import Footer from "./Footer";
|
|
import PrevNext from "./PrevNext";
|
|
|
|
// Note: if you want to add `action`,
|
|
// pls contact @zombieJ or @thinkasany first.
|
|
|
|
const PreviewImage = ({
|
|
fallback,
|
|
src,
|
|
imgRef,
|
|
...props
|
|
}) => {
|
|
const [getImgRef, srcAndOnload] = useStatus({
|
|
src,
|
|
fallback
|
|
});
|
|
return /*#__PURE__*/React.createElement("img", _extends({
|
|
ref: ref => {
|
|
imgRef.current = ref;
|
|
getImgRef(ref);
|
|
}
|
|
}, props, srcAndOnload));
|
|
};
|
|
const Preview = props => {
|
|
const {
|
|
prefixCls,
|
|
rootClassName,
|
|
src,
|
|
alt,
|
|
imageInfo,
|
|
fallback,
|
|
movable = true,
|
|
onClose,
|
|
open,
|
|
afterOpenChange,
|
|
maskClosable = true,
|
|
icons = {},
|
|
closeIcon,
|
|
getContainer,
|
|
current = 0,
|
|
count = 1,
|
|
countRender,
|
|
scaleStep = 0.5,
|
|
minScale = 1,
|
|
maxScale = 50,
|
|
motionName = 'fade',
|
|
imageRender,
|
|
imgCommonProps,
|
|
actionsRender,
|
|
onTransform,
|
|
onChange,
|
|
classNames = {},
|
|
styles = {},
|
|
mousePosition,
|
|
zIndex,
|
|
focusTrap = true
|
|
} = props;
|
|
const imgRef = useRef();
|
|
const wrapperRef = useRef(null);
|
|
const triggerRef = useRef(null);
|
|
const groupContext = useContext(PreviewGroupContext);
|
|
const showLeftOrRightSwitches = groupContext && count > 1;
|
|
const showOperationsProgress = groupContext && count >= 1;
|
|
|
|
// ======================== Transform =========================
|
|
const [enableTransition, setEnableTransition] = useState(true);
|
|
const {
|
|
transform,
|
|
resetTransform,
|
|
updateTransform,
|
|
dispatchZoomChange
|
|
} = useImageTransform(imgRef, minScale, maxScale, onTransform);
|
|
const {
|
|
isMoving,
|
|
onMouseDown,
|
|
onWheel
|
|
} = useMouseEvent(imgRef, movable, open, scaleStep, transform, updateTransform, dispatchZoomChange);
|
|
const {
|
|
isTouching,
|
|
onTouchStart,
|
|
onTouchMove,
|
|
onTouchEnd
|
|
} = useTouchEvent(imgRef, movable, open, minScale, transform, updateTransform, dispatchZoomChange);
|
|
const {
|
|
rotate,
|
|
scale
|
|
} = transform;
|
|
useEffect(() => {
|
|
if (!enableTransition) {
|
|
setEnableTransition(true);
|
|
}
|
|
}, [enableTransition]);
|
|
useEffect(() => {
|
|
if (!open) {
|
|
resetTransform('close');
|
|
}
|
|
}, [open]);
|
|
|
|
// ========================== Image ===========================
|
|
const onDoubleClick = event => {
|
|
if (open) {
|
|
if (scale !== 1) {
|
|
updateTransform({
|
|
x: 0,
|
|
y: 0,
|
|
scale: 1
|
|
}, 'doubleClick');
|
|
} else {
|
|
dispatchZoomChange(BASE_SCALE_RATIO + scaleStep, 'doubleClick', event.clientX, event.clientY);
|
|
}
|
|
}
|
|
};
|
|
const imgNode = /*#__PURE__*/React.createElement(PreviewImage, _extends({}, imgCommonProps, {
|
|
width: props.width,
|
|
height: props.height,
|
|
imgRef: imgRef,
|
|
className: `${prefixCls}-img`,
|
|
alt: alt,
|
|
style: {
|
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scale3d(${transform.flipX ? '-' : ''}${scale}, ${transform.flipY ? '-' : ''}${scale}, 1) rotate(${rotate}deg)`,
|
|
transitionDuration: (!enableTransition || isTouching) && '0s'
|
|
},
|
|
fallback: fallback,
|
|
src: src,
|
|
onWheel: onWheel,
|
|
onMouseDown: onMouseDown,
|
|
onDoubleClick: onDoubleClick,
|
|
onTouchStart: onTouchStart,
|
|
onTouchMove: onTouchMove,
|
|
onTouchEnd: onTouchEnd,
|
|
onTouchCancel: onTouchEnd
|
|
}));
|
|
const image = {
|
|
url: src,
|
|
alt,
|
|
...imageInfo
|
|
};
|
|
|
|
// ======================== Operation =========================
|
|
// >>>>> Actions
|
|
const onZoomIn = () => {
|
|
dispatchZoomChange(BASE_SCALE_RATIO + scaleStep, 'zoomIn');
|
|
};
|
|
const onZoomOut = () => {
|
|
dispatchZoomChange(BASE_SCALE_RATIO / (BASE_SCALE_RATIO + scaleStep), 'zoomOut');
|
|
};
|
|
const onRotateRight = () => {
|
|
updateTransform({
|
|
rotate: rotate + 90
|
|
}, 'rotateRight');
|
|
};
|
|
const onRotateLeft = () => {
|
|
updateTransform({
|
|
rotate: rotate - 90
|
|
}, 'rotateLeft');
|
|
};
|
|
const onFlipX = () => {
|
|
updateTransform({
|
|
flipX: !transform.flipX
|
|
}, 'flipX');
|
|
};
|
|
const onFlipY = () => {
|
|
updateTransform({
|
|
flipY: !transform.flipY
|
|
}, 'flipY');
|
|
};
|
|
const onReset = () => {
|
|
resetTransform('reset');
|
|
};
|
|
const onActive = offset => {
|
|
const nextCurrent = current + offset;
|
|
if (nextCurrent >= 0 && nextCurrent <= count - 1) {
|
|
setEnableTransition(false);
|
|
resetTransform(offset < 0 ? 'prev' : 'next');
|
|
onChange?.(nextCurrent, current);
|
|
}
|
|
};
|
|
|
|
// >>>>> Effect: Keyboard
|
|
const onKeyDown = useEvent(event => {
|
|
if (open) {
|
|
const {
|
|
keyCode
|
|
} = event;
|
|
if (showLeftOrRightSwitches) {
|
|
if (keyCode === KeyCode.LEFT) {
|
|
onActive(-1);
|
|
} else if (keyCode === KeyCode.RIGHT) {
|
|
onActive(1);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
useEffect(() => {
|
|
if (open) {
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => {
|
|
window.removeEventListener('keydown', onKeyDown);
|
|
};
|
|
}
|
|
}, [open]);
|
|
|
|
// ======================= Lock Scroll ========================
|
|
const [lockScroll, setLockScroll] = useState(false);
|
|
React.useEffect(() => {
|
|
if (open) {
|
|
setLockScroll(true);
|
|
}
|
|
}, [open]);
|
|
const onVisibleChanged = nextVisible => {
|
|
if (!nextVisible) {
|
|
setLockScroll(false);
|
|
|
|
// Restore focus to the trigger element after leave animation
|
|
triggerRef.current?.focus?.();
|
|
triggerRef.current = null;
|
|
}
|
|
afterOpenChange?.(nextVisible);
|
|
};
|
|
|
|
// ========================== Portal ==========================
|
|
const [portalRender, setPortalRender] = useState(false);
|
|
useLayoutEffect(() => {
|
|
if (open) {
|
|
setPortalRender(true);
|
|
}
|
|
}, [open]);
|
|
const onEsc = ({
|
|
top
|
|
}) => {
|
|
if (top) {
|
|
onClose?.();
|
|
}
|
|
};
|
|
|
|
// =========================== Focus ============================
|
|
useLayoutEffect(() => {
|
|
if (open) {
|
|
triggerRef.current = document.activeElement;
|
|
}
|
|
}, [open]);
|
|
useLockFocus(focusTrap && open && portalRender, () => wrapperRef.current);
|
|
|
|
// ========================== Render ==========================
|
|
const bodyStyle = {
|
|
...styles.body
|
|
};
|
|
if (mousePosition) {
|
|
bodyStyle.transformOrigin = `${mousePosition.x}px ${mousePosition.y}px`;
|
|
}
|
|
return /*#__PURE__*/React.createElement(Portal, {
|
|
open: portalRender && open,
|
|
autoDestroy: false,
|
|
getContainer: getContainer,
|
|
autoLock: lockScroll,
|
|
onEsc: onEsc
|
|
}, /*#__PURE__*/React.createElement(CSSMotion, {
|
|
motionName: motionName,
|
|
visible: portalRender && open,
|
|
motionAppear: true,
|
|
motionEnter: true,
|
|
motionLeave: true,
|
|
onVisibleChanged: onVisibleChanged
|
|
}, ({
|
|
className: motionClassName,
|
|
style: motionStyle
|
|
}) => {
|
|
const mergedStyle = {
|
|
...styles.root,
|
|
...motionStyle
|
|
};
|
|
if (zIndex) {
|
|
mergedStyle.zIndex = zIndex;
|
|
}
|
|
return /*#__PURE__*/React.createElement("div", {
|
|
ref: wrapperRef,
|
|
className: clsx(prefixCls, rootClassName, classNames.root, motionClassName, {
|
|
[`${prefixCls}-movable`]: movable,
|
|
[`${prefixCls}-moving`]: isMoving
|
|
}),
|
|
style: mergedStyle,
|
|
role: "dialog",
|
|
"aria-modal": "true",
|
|
"aria-label": alt,
|
|
tabIndex: -1
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
className: clsx(`${prefixCls}-mask`, classNames.mask),
|
|
style: styles.mask,
|
|
onClick: maskClosable ? onClose : undefined
|
|
}), /*#__PURE__*/React.createElement("div", {
|
|
className: clsx(`${prefixCls}-body`, classNames.body),
|
|
style: bodyStyle
|
|
}, imageRender ? imageRender(imgNode, {
|
|
transform,
|
|
image,
|
|
...(groupContext ? {
|
|
current
|
|
} : {})
|
|
}) : imgNode), closeIcon !== false && closeIcon !== null && /*#__PURE__*/React.createElement(CloseBtn, {
|
|
prefixCls: prefixCls,
|
|
icon: closeIcon === true ? icons.close : closeIcon || icons.close,
|
|
onClick: onClose,
|
|
className: classNames.close,
|
|
style: styles.close
|
|
}), showLeftOrRightSwitches && /*#__PURE__*/React.createElement(PrevNext, {
|
|
prefixCls: prefixCls,
|
|
current: current,
|
|
count: count,
|
|
icons: icons,
|
|
onActive: onActive
|
|
}), /*#__PURE__*/React.createElement(Footer, {
|
|
prefixCls: prefixCls,
|
|
showProgress: showOperationsProgress,
|
|
current: current,
|
|
count: count,
|
|
showSwitch: showLeftOrRightSwitches
|
|
// Style
|
|
,
|
|
classNames: classNames,
|
|
styles: styles
|
|
// Render
|
|
,
|
|
image: image,
|
|
transform: transform,
|
|
icons: icons,
|
|
countRender: countRender,
|
|
actionsRender: actionsRender
|
|
// Scale
|
|
,
|
|
scale: scale,
|
|
minScale: minScale,
|
|
maxScale: maxScale
|
|
// Actions
|
|
,
|
|
onActive: onActive,
|
|
onFlipY: onFlipY,
|
|
onFlipX: onFlipX,
|
|
onRotateLeft: onRotateLeft,
|
|
onRotateRight: onRotateRight,
|
|
onZoomOut: onZoomOut,
|
|
onZoomIn: onZoomIn,
|
|
onClose: onClose,
|
|
onReset: onReset
|
|
}));
|
|
}));
|
|
};
|
|
export default Preview; |