This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
MIT LICENSE
Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+224
View File
@@ -0,0 +1,224 @@
# rc-image
React Image.
<!-- prettier-ignore -->
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
[![build status][github-actions-image]][github-actions-url]
[![Codecov][codecov-image]][codecov-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
[![dumi][dumi-image]][dumi-url]
[npm-image]: http://img.shields.io/npm/v/rc-image.svg?style=flat-square
[npm-url]: http://npmjs.org/package/rc-image
[github-actions-image]: https://github.com/react-component/image/actions/workflows/react-component-ci.yml/badge.svg
[github-actions-url]: https://github.com/react-component/image/actions/workflows/react-component-ci.yml
[codecov-image]: https://img.shields.io/codecov/c/gh/react-component/image?style=flat-square
[codecov-url]: https://codecov.io/gh/react-component/image
[download-image]: https://img.shields.io/npm/dm/rc-image.svg?style=flat-square
[download-url]: https://npmjs.org/package/rc-image
[bundlephobia-url]: https://bundlephobia.com/result?p=rc-image
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-image
[dumi-url]: https://github.com/umijs/dumi
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
## Feature
- [x] Placeholder
- [x] Preview
- [x] Rotate
- [x] Zoom
- [x] Flip
- [x] Fallback
- [x] Multiple Preview
## install
[![rc-image](https://nodei.co/npm/rc-image.png)](https://npmjs.org/package/rc-image)
## Usage
```bash
npm install
npm start
```
```js
import Image from '@rc-component/image';
export default () => (
<Image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />
);
```
## API
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| preview | boolean \| [PreviewType](#PreviewType) | true | Whether to show preview |
| prefixCls | string | rc-image | Classname prefix |
| placeholder | boolean \| ReactElement | - | if `true` will set default placeholder or use `ReactElement` set customize placeholder |
| fallback | string | - | Load failed src |
| previewPrefixCls | string | rc-image-preview | Preview classname prefix |
| onError | (event: Event) => void | - | Load failed callback |
### PreviewType
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| open | boolean | - | Whether the preview is open or not |
| closeIcon | React.ReactNode | - | Custom close icon |
| maskClosable | boolean | true | Whether to close preview on clicking mask |
| src | string | - | Customize preview src |
| movable | boolean | true | Enable drag |
| scaleStep | number | 0.5 | The number to which the scale is increased or decreased |
| minScale | number | 1 | Min scale |
| maxScale | number | 50 | Max scale |
| forceRender | boolean | - | Force render preview |
| getContainer | string \| HTMLElement \| (() => HTMLElement) \| false | document.body | Return the mount node for preview |
| imageRender | (originalNode: React.ReactElement, info: { transform: [TransformType](#TransformType) }) => React.ReactNode | - | Customize image |
| actionsRender | (originalNode: React.ReactElement, info: Omit<[ToolbarRenderInfoType](#ToolbarRenderInfoType), 'current' \| 'total'>) => React.ReactNode | - | Customize toolbar |
| onOpenChange | (open: boolean, prevVisible: boolean) => void | - | Callback when open is changed |
| onTransform | { transform: [TransformType](#TransformType), action: [TransformAction](#TransformAction) } | - | Callback when transform is changed |
## Image.PreviewGroup
preview the merged src
```js
import Image from '@rc-component/image';
export default () => (
<Image.PreviewGroup>
<Image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" />
<Image src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*P0S-QIRUbsUAAAAAAAAAAABkARQnAQ" />
</Image.PreviewGroup>
);
```
### API
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| preview | boolean \| [PreviewGroupType](#PreviewGroupType) | true | Whether to show preview, <br> current: If Preview the show img index, default 0 |
| previewPrefixCls | string | rc-image-preview | Preview classname prefix |
| icons | { [iconKey]?: ReactNode } | - | Icons in the top operation bar, iconKey: 'rotateLeft' \| 'rotateRight' \| 'zoomIn' \| 'zoomOut' \| 'close' \| 'left' \| 'right' |
| fallback | string | - | Load failed src |
| items | (string \| { src: string, alt: string, crossOrigin: string, ... })[] | - | preview group |
### PreviewGroupType
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| open | boolean | - | Whether the preview is open or not |
| movable | boolean | true | Enable drag |
| current | number | - | Current index |
| closeIcon | React.ReactNode | - | Custom close icon |
| maskClosable | boolean | true | Whether to close preview on clicking mask |
| scaleStep | number | 0.5 | The number to which the scale is increased or decreased |
| minScale | number | 1 | Min scale |
| maxScale | number | 50 | Max scale |
| forceRender | boolean | - | Force render preview |
| getContainer | string \| HTMLElement \| (() => HTMLElement) \| false | document.body | Return the mount node for preview |
| countRender | (current: number, total: number) => ReactNode | - | Customize count |
| imageRender | (originalNode: React.ReactElement, info: { transform: [TransformType](#TransformType), current: number }) => React.ReactNode | - | Customize image |
| actionsRender | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#ToolbarRenderInfoType)) => React.ReactNode | - | Customize toolbar |
| onOpenChange | (open: boolean, prevVisible: boolean, current: number) => void | - | Callback when open is changed |
| onTransform | { transform: [TransformType](#TransformType), action: [TransformAction](#TransformAction) } | - | Callback when transform is changed |
### TransformType
```typescript
{
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
}
```
### TransformAction
```typescript
type TransformAction =
| 'flipY'
| 'flipX'
| 'rotateLeft'
| 'rotateRight'
| 'zoomIn'
| 'zoomOut'
| 'close'
| 'prev'
| 'next'
| 'wheel'
| 'doubleClick'
| 'move'
| 'dragRebound';
```
### ToolbarRenderInfoType
```typescript
{
icons: {
prevIcon?: React.ReactNode;
nextIcon?: React.ReactNode;
flipYIcon: React.ReactNode;
flipXIcon: React.ReactNode;
rotateLeftIcon: React.ReactNode;
rotateRightIcon: React.ReactNode;
zoomOutIcon: React.ReactNode;
zoomInIcon: React.ReactNode;
};
actions: {
onActive?: (offset: number) => void;
onFlipY: () => void;
onFlipX: () => void;
onRotateLeft: () => void;
onRotateRight: () => void;
onZoomOut: () => void;
onZoomIn: () => void;
onClose: () => void;
onReset: () => void;
};
transform: {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
},
current: number;
total: number;
}
```
## Example
http://localhost:8003/examples/
## Test Case
```
npm test
```
## Coverage
```
npm run coverage
```
## License
rc-image is released under the MIT license.
## 🤝 Contributing
<a href="https://openomy.app/github/react-component/image" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://www.openomy.app/svg?repo=react-component/image&chart=bubble&latestMonth=24" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
+224
View File
@@ -0,0 +1,224 @@
.rc-image-preview {
position: fixed;
overflow: hidden;
user-select: none;
inset: 0;
z-index: 1055;
}
.rc-image-preview-mask {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
inset: 0;
}
.rc-image-preview-body {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
inset: 0;
}
.rc-image-preview-body > * {
pointer-events: auto;
}
.rc-image-preview-img {
max-width: 100%;
max-height: 70%;
}
.rc-image-preview-close {
position: absolute;
top: 16px;
right: 16px;
z-index: 1;
width: 32px;
height: 32px;
color: #fff;
background: rgba(0, 0, 0, 0.3);
border: 0;
border-radius: 99px;
cursor: pointer;
}
.rc-image-preview-switch {
position: absolute;
top: 50%;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: #fff;
background: rgba(0, 0, 0, 0.3);
border: 0;
padding: 0;
border-radius: 9999px;
transform: translateY(-50%);
cursor: pointer;
font: inherit;
}
.rc-image-preview-switch-disabled {
cursor: default;
opacity: 0.1;
}
.rc-image-preview-switch-prev {
inset-inline-start: 0;
}
.rc-image-preview-switch-next {
inset-inline-end: 0;
}
.rc-image-preview-footer {
position: absolute;
bottom: 24px;
left: 50%;
z-index: 1;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
transform: translateX(-50%);
}
.rc-image-preview-progress {
color: #fff;
}
.rc-image-preview-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
}
.rc-image-preview-actions-action {
color: #fff;
cursor: pointer;
border: 0;
padding: 0;
background: transparent;
font: inherit;
}
.rc-image-preview-actions-action-disabled {
cursor: default;
opacity: 0.5;
}
.rc-image-preview.fade {
transition: all 0.3s;
}
.rc-image-preview.fade-enter,
.rc-image-preview.fade-appear {
opacity: 0;
}
.rc-image-preview.fade-enter .rc-image-preview-body,
.rc-image-preview.fade-appear .rc-image-preview-body {
transform: scale(0);
}
.rc-image-preview.fade-enter-active,
.rc-image-preview.fade-appear-active {
opacity: 1;
}
.rc-image-preview.fade-enter-active .rc-image-preview-body,
.rc-image-preview.fade-appear-active .rc-image-preview-body {
transform: scale(1);
transition: all 0.3s;
}
.rc-image-preview.fade-leave {
opacity: 1;
}
.rc-image-preview.fade-leave-active {
opacity: 0;
}
.rc-image {
position: relative;
display: inline-flex;
}
.rc-image-img {
width: 100%;
height: auto;
overflow: hidden;
}
.rc-image-img-placeholder {
background-color: #f3f3f3;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjhweCIgaGVpZ2h0PSIyMnB4IiB2aWV3Qm94PSIwIDAgMjggMjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDU1LjIgKDc4MTgxKSAtIGh0dHBzOi8vc2tldGNoYXBwLmNvbSAtLT4KICAgIDx0aXRsZT5pbWFnZS1maWxs5aSH5Lu9PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9Iuafpeeci+WbvueJh+S8mOWMljQuMCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IuWKoOi9veWbvueJhyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTU3Mi4wMDAwMDAsIC01MDYuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJpbWFnZS1maWxs5aSH5Lu9IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1NzAuMDAwMDAwLCA1MDEuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjMDAwMDAwIiBvcGFjaXR5PSIwIiB4PSIwIiB5PSIwIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjwvcmVjdD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOSw1IEwzLDUgQzIuNDQ2ODc1LDUgMiw1LjQ0Njg3NSAyLDYgTDIsMjYgQzIsMjYuNTUzMTI1IDIuNDQ2ODc1LDI3IDMsMjcgTDI5LDI3IEMyOS41NTMxMjUsMjcgMzAsMjYuNTUzMTI1IDMwLDI2IEwzMCw2IEMzMCw1LjQ0Njg3NSAyOS41NTMxMjUsNSAyOSw1IFogTTEwLjU2MjUsOS41IEMxMS42NjU2MjUsOS41IDEyLjU2MjUsMTAuMzk2ODc1IDEyLjU2MjUsMTEuNSBDMTIuNTYyNSwxMi42MDMxMjUgMTEuNjY1NjI1LDEzLjUgMTAuNTYyNSwxMy41IEM5LjQ1OTM3NSwxMy41IDguNTYyNSwxMi42MDMxMjUgOC41NjI1LDExLjUgQzguNTYyNSwxMC4zOTY4NzUgOS40NTkzNzUsOS41IDEwLjU2MjUsOS41IFogTTI2LjYyMTg3NSwyMy4xNTkzNzUgQzI2LjU3ODEyNSwyMy4xOTY4NzUgMjYuNTE4NzUsMjMuMjE4NzUgMjYuNDU5Mzc1LDIzLjIxODc1IEw1LjUzNzUsMjMuMjE4NzUgQzUuNCwyMy4yMTg3NSA1LjI4NzUsMjMuMTA2MjUgNS4yODc1LDIyLjk2ODc1IEM1LjI4NzUsMjIuOTA5Mzc1IDUuMzA5Mzc1LDIyLjg1MzEyNSA1LjM0Njg3NSwyMi44MDYyNSBMMTAuNjY4NzUsMTYuNDkzNzUgQzEwLjc1NjI1LDE2LjM4NzUgMTAuOTE1NjI1LDE2LjM3NSAxMS4wMjE4NzUsMTYuNDYyNSBDMTEuMDMxMjUsMTYuNDcxODc1IDExLjA0Mzc1LDE2LjQ4MTI1IDExLjA1MzEyNSwxNi40OTM3NSBMMTQuMTU5Mzc1LDIwLjE4MTI1IEwxOS4xLDE0LjMyMTg3NSBDMTkuMTg3NSwxNC4yMTU2MjUgMTkuMzQ2ODc1LDE0LjIwMzEyNSAxOS40NTMxMjUsMTQuMjkwNjI1IEMxOS40NjI1LDE0LjMgMTkuNDc1LDE0LjMwOTM3NSAxOS40ODQzNzUsMTQuMzIxODc1IEwyNi42NTkzNzUsMjIuODA5Mzc1IEMyNi43NDA2MjUsMjIuOTEyNSAyNi43MjgxMjUsMjMuMDcxODc1IDI2LjYyMTg3NSwyMy4xNTkzNzUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRThFOEU4Ij48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==);
background-repeat: no-repeat;
background-position: center center;
}
.rc-image-cover {
position: absolute;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
background-color: rgba(0, 0, 0, 0.5);
transition: background-color 0.3s;
}
.rc-image-cover-top {
top: 0;
left: 0;
right: 0;
height: max-content;
justify-content: flex-start;
padding: 5px;
}
.rc-image-cover-bottom {
bottom: 0;
left: 0;
right: 0;
height: max-content;
justify-content: flex-end;
padding: 5px;
}
.rc-image-cover-center {
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
height: 100%;
align-items: center;
justify-content: center;
}
.rc-image-placeholder {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.rc-image-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.rc-image:hover .rc-image-mask {
opacity: 1;
}
@keyframes rcImageZoomIn {
0% {
transform: scale(0.2);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes rcImageZoomOut {
0% {
transform: scale(1);
}
100% {
transform: scale(0.2);
opacity: 0;
}
}
+47
View File
@@ -0,0 +1,47 @@
import * as React from 'react';
import type { InternalPreviewConfig, PreviewSemanticName, ToolbarRenderInfoType } from './Preview';
import PreviewGroup from './PreviewGroup';
import type { TransformType } from './hooks/useImageTransform';
export interface ImgInfo {
url: string;
alt: string;
width: string | number;
height: string | number;
}
export interface CoverConfig {
coverNode?: React.ReactNode;
placement?: 'top' | 'bottom' | 'center';
}
export interface PreviewConfig extends Omit<InternalPreviewConfig, 'countRender'> {
cover?: React.ReactNode | CoverConfig;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
image: ImgInfo;
}) => React.ReactNode;
actionsRender?: (originalNode: React.ReactElement, info: Omit<ToolbarRenderInfoType, 'current' | 'total'>) => React.ReactNode;
onOpenChange?: (open: boolean) => void;
}
export type SemanticName = 'root' | 'image' | 'cover';
export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'placeholder' | 'onClick' | 'onKeyDown'> {
prefixCls?: string;
previewPrefixCls?: string;
rootClassName?: string;
classNames?: Partial<Record<SemanticName, string> & {
popup?: Partial<Record<PreviewSemanticName, string>>;
}>;
styles?: Partial<Record<SemanticName, React.CSSProperties> & {
popup?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
}>;
src?: string;
placeholder?: React.ReactNode;
fallback?: string;
preview?: boolean | PreviewConfig;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
}
interface CompoundedComponent<P> extends React.FC<P> {
PreviewGroup: typeof PreviewGroup;
}
declare const ImageInternal: CompoundedComponent<ImageProps>;
export default ImageInternal;
+191
View File
@@ -0,0 +1,191 @@
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 useControlledState from "@rc-component/util/es/hooks/useControlledState";
import { clsx } from 'clsx';
import * as React from 'react';
import { useContext, useMemo, useState } from 'react';
import Preview from "./Preview";
import PreviewGroup from "./PreviewGroup";
import { COMMON_PROPS } from "./common";
import { PreviewGroupContext } from "./context";
import useRegisterImage from "./hooks/useRegisterImage";
import useStatus from "./hooks/useStatus";
const ImageInternal = props => {
const {
// Misc
prefixCls = 'rc-image',
previewPrefixCls = `${prefixCls}-preview`,
// Style
rootClassName,
className,
style,
classNames = {},
styles = {},
width,
height,
// Image
src: imgSrc,
alt,
placeholder,
fallback,
// Preview
preview = true,
// Events
onClick,
onError,
onKeyDown,
...otherProps
} = props;
const groupContext = useContext(PreviewGroupContext);
// ========================== Preview ===========================
const canPreview = !!preview;
const {
src: previewSrc,
open: previewOpen,
onOpenChange: onPreviewOpenChange,
cover,
rootClassName: previewRootClassName,
...restProps
} = preview && typeof preview === 'object' ? preview : {};
const coverPlacement = typeof cover === 'object' && cover.placement ? cover.placement || 'center' : 'center';
const coverNode = typeof cover === 'object' && cover.coverNode ? cover.coverNode : cover;
// ============================ Open ============================
const [isShowPreview, setShowPreview] = useControlledState(!!previewOpen, previewOpen);
const [mousePosition, setMousePosition] = useState(null);
const triggerPreviewOpen = nextOpen => {
setShowPreview(nextOpen);
onPreviewOpenChange?.(nextOpen);
};
const onPreviewClose = () => {
triggerPreviewOpen(false);
};
// ========================= ImageProps =========================
const isCustomPlaceholder = placeholder && placeholder !== true;
const src = previewSrc ?? imgSrc;
const [getImgRef, srcAndOnload, status] = useStatus({
src: imgSrc,
isCustomPlaceholder,
fallback
});
const imgCommonProps = useMemo(() => {
const obj = {};
COMMON_PROPS.forEach(prop => {
if (props[prop] !== undefined) {
obj[prop] = props[prop];
}
});
return obj;
}, COMMON_PROPS.map(prop => props[prop]));
// ========================== Register ==========================
const registerData = useMemo(() => ({
...imgCommonProps,
src
}), [src, imgCommonProps]);
const imageId = useRegisterImage(canPreview, registerData);
// ========================== Preview ===========================
const onPreview = e => {
const rect = e.target.getBoundingClientRect();
const left = rect.x + rect.width / 2;
const top = rect.y + rect.height / 2;
if (groupContext) {
groupContext.onPreview(imageId, src, left, top);
} else {
setMousePosition({
x: left,
y: top
});
triggerPreviewOpen(true);
}
onClick?.(e);
};
// ======================= Keyboard Preview =====================
const onPreviewKeyDown = event => {
onKeyDown?.(event);
if (!canPreview) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
const rect = event.target.getBoundingClientRect();
const left = rect.x + rect.width / 2;
const top = rect.y + rect.height / 2;
if (groupContext) {
groupContext.onPreview(imageId, src, left, top);
} else {
setMousePosition({
x: left,
y: top
});
triggerPreviewOpen(true);
}
}
};
// =========================== Render ===========================
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", _extends({}, otherProps, {
className: clsx(prefixCls, rootClassName, classNames.root, {
[`${prefixCls}-error`]: status === 'error'
}),
onClick: canPreview ? onPreview : onClick,
role: canPreview ? 'button' : otherProps.role,
tabIndex: canPreview && otherProps.tabIndex == null ? 0 : otherProps.tabIndex,
"aria-label": canPreview ? otherProps['aria-label'] ?? alt : otherProps['aria-label'],
onKeyDown: onPreviewKeyDown,
style: {
width,
height,
...styles.root
}
}), /*#__PURE__*/React.createElement("img", _extends({}, imgCommonProps, {
className: clsx(`${prefixCls}-img`, {
[`${prefixCls}-img-placeholder`]: placeholder === true
}, classNames.image, className),
style: {
height,
...styles.image,
...style
},
ref: getImgRef
}, srcAndOnload, {
width: width,
height: height,
onError: onError
})), status === 'loading' && /*#__PURE__*/React.createElement("div", {
"aria-hidden": "true",
className: `${prefixCls}-placeholder`
}, placeholder), cover !== false && canPreview && /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-cover`, classNames.cover, `${prefixCls}-cover-${coverPlacement}`),
style: {
display: style?.display === 'none' ? 'none' : undefined,
...styles.cover
}
}, coverNode)), !groupContext && canPreview && /*#__PURE__*/React.createElement(Preview, _extends({
"aria-hidden": !isShowPreview,
open: isShowPreview,
prefixCls: previewPrefixCls,
onClose: onPreviewClose,
mousePosition: mousePosition,
src: src,
alt: alt,
imageInfo: {
width,
height
},
fallback: fallback,
imgCommonProps: imgCommonProps
}, restProps, {
classNames: classNames?.popup,
styles: styles?.popup,
rootClassName: clsx(previewRootClassName, rootClassName)
})));
};
ImageInternal.PreviewGroup = PreviewGroup;
if (process.env.NODE_ENV !== 'production') {
ImageInternal.displayName = 'Image';
}
export default ImageInternal;
@@ -0,0 +1,9 @@
import * as React from 'react';
export interface CloseBtnProps {
prefixCls: string;
icon?: React.ReactNode;
onClick: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
}
export default function CloseBtn(props: CloseBtnProps): React.JSX.Element;
@@ -0,0 +1,16 @@
import { clsx } from 'clsx';
import * as React from 'react';
export default function CloseBtn(props) {
const {
prefixCls,
icon,
onClick,
className,
style
} = props;
return /*#__PURE__*/React.createElement("button", {
className: clsx(`${prefixCls}-close`, className),
style: style,
onClick: onClick
}, icon);
}
@@ -0,0 +1,23 @@
import * as React from 'react';
import type { Actions, PreviewProps } from '.';
import type { ImgInfo } from '../Image';
import type { TransformType } from '../hooks/useImageTransform';
export type FooterSemanticName = 'footer' | 'actions';
export interface FooterProps extends Actions {
prefixCls: string;
showProgress: boolean;
countRender?: PreviewProps['countRender'];
actionsRender?: PreviewProps['actionsRender'];
current: number;
count: number;
showSwitch: boolean;
icons: PreviewProps['icons'];
scale: number;
minScale: number;
maxScale: number;
image: ImgInfo;
transform: TransformType;
classNames: Partial<Record<FooterSemanticName, string>>;
styles: Partial<Record<FooterSemanticName, React.CSSProperties>>;
}
export default function Footer(props: FooterProps): React.JSX.Element;
+153
View File
@@ -0,0 +1,153 @@
import { clsx } from 'clsx';
import * as React from 'react';
export default function Footer(props) {
// 修改解构,添加缺失的属性,并提供默认值
const {
prefixCls,
showProgress,
current,
count,
showSwitch,
// Style
classNames,
styles,
// render
icons,
image,
transform,
countRender,
actionsRender,
// Scale
scale,
minScale,
maxScale,
// Actions
onActive,
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onClose,
onReset
} = props;
const {
left,
right,
prev,
next,
flipY,
flipX,
rotateLeft,
rotateRight,
zoomOut,
zoomIn
} = icons;
// ========================== Render ==========================
// >>>>> Progress
const progressNode = showProgress && /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-progress`
}, countRender ? countRender(current + 1, count) : /*#__PURE__*/React.createElement("bdi", null, `${current + 1} / ${count}`));
// >>>>> Actions
const actionCls = `${prefixCls}-actions-action`;
const renderOperation = ({
type,
disabled,
onClick,
icon
}) => {
return /*#__PURE__*/React.createElement("button", {
type: "button",
key: type,
className: clsx(actionCls, `${actionCls}-${type}`, {
[`${actionCls}-disabled`]: !!disabled
}),
onClick: onClick,
disabled: !!disabled,
"aria-label": type
}, icon);
};
const switchPrevNode = showSwitch ? renderOperation({
icon: prev ?? left,
onClick: () => onActive(-1),
type: 'prev',
disabled: current === 0
}) : undefined;
const switchNextNode = showSwitch ? renderOperation({
icon: next ?? right,
onClick: () => onActive(1),
type: 'next',
disabled: current === count - 1
}) : undefined;
const flipYNode = renderOperation({
icon: flipY,
onClick: onFlipY,
type: 'flipY'
});
const flipXNode = renderOperation({
icon: flipX,
onClick: onFlipX,
type: 'flipX'
});
const rotateLeftNode = renderOperation({
icon: rotateLeft,
onClick: onRotateLeft,
type: 'rotateLeft'
});
const rotateRightNode = renderOperation({
icon: rotateRight,
onClick: onRotateRight,
type: 'rotateRight'
});
const zoomOutNode = renderOperation({
icon: zoomOut,
onClick: onZoomOut,
type: 'zoomOut',
disabled: scale <= minScale
});
const zoomInNode = renderOperation({
icon: zoomIn,
onClick: onZoomIn,
type: 'zoomIn',
disabled: scale === maxScale
});
const actionsNode = /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-actions`, classNames.actions),
style: styles.actions
}, flipYNode, flipXNode, rotateLeftNode, rotateRightNode, zoomOutNode, zoomInNode);
// >>>>> Render
return /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-footer`, classNames.footer),
style: styles.footer
}, progressNode, actionsRender ? actionsRender(actionsNode, {
icons: {
prevIcon: switchPrevNode,
nextIcon: switchNextNode,
flipYIcon: flipYNode,
flipXIcon: flipXNode,
rotateLeftIcon: rotateLeftNode,
rotateRightIcon: rotateRightNode,
zoomOutIcon: zoomOutNode,
zoomInIcon: zoomInNode
},
actions: {
onActive,
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onReset,
onClose
},
transform,
current,
total: count,
image
}) : actionsNode);
}
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { OperationIcons } from '.';
export interface PrevNextProps {
prefixCls: string;
onActive: (offset: number) => void;
current: number;
count: number;
icons: OperationIcons;
}
export default function PrevNext(props: PrevNextProps): React.JSX.Element;
@@ -0,0 +1,33 @@
import { clsx } from 'clsx';
import * as React from 'react';
export default function PrevNext(props) {
const {
prefixCls,
onActive,
current,
count,
icons: {
left,
right,
prev,
next
}
} = props;
const switchCls = `${prefixCls}-switch`;
const prevDisabled = current === 0;
const nextDisabled = current === count - 1;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
className: clsx(switchCls, `${switchCls}-prev`, {
[`${switchCls}-disabled`]: prevDisabled
}),
onClick: () => onActive(-1),
disabled: prevDisabled
}, prev ?? left), /*#__PURE__*/React.createElement("button", {
type: "button",
className: clsx(switchCls, `${switchCls}-next`, {
[`${switchCls}-disabled`]: nextDisabled
}),
onClick: () => onActive(1),
disabled: nextDisabled
}, next ?? right));
}
+103
View File
@@ -0,0 +1,103 @@
import { type PortalProps } from '@rc-component/portal';
import React from 'react';
import type { TransformAction, TransformType } from '../hooks/useImageTransform';
import type { ImgInfo } from '../Image';
import { type FooterSemanticName } from './Footer';
export type PreviewSemanticName = 'root' | 'mask' | 'body' | 'close' | FooterSemanticName;
export interface OperationIcons {
rotateLeft?: React.ReactNode;
rotateRight?: React.ReactNode;
zoomIn?: React.ReactNode;
zoomOut?: React.ReactNode;
close?: React.ReactNode;
prev?: React.ReactNode;
next?: React.ReactNode;
/** @deprecated Please use `prev` instead */
left?: React.ReactNode;
/** @deprecated Please use `next` instead */
right?: React.ReactNode;
flipX?: React.ReactNode;
flipY?: React.ReactNode;
}
export interface Actions {
onActive: (offset: number) => void;
onFlipY: () => void;
onFlipX: () => void;
onRotateLeft: () => void;
onRotateRight: () => void;
onZoomOut: () => void;
onZoomIn: () => void;
onClose: () => void;
onReset: () => void;
}
export type ToolbarRenderInfoType = {
icons: {
prevIcon?: React.ReactNode;
nextIcon?: React.ReactNode;
flipYIcon: React.ReactNode;
flipXIcon: React.ReactNode;
rotateLeftIcon: React.ReactNode;
rotateRightIcon: React.ReactNode;
zoomOutIcon: React.ReactNode;
zoomInIcon: React.ReactNode;
};
actions: Actions;
transform: TransformType;
current: number;
total: number;
image: ImgInfo;
};
export interface InternalPreviewConfig {
/** Better to use `classNames.root` instead */
rootClassName?: string;
src?: string;
alt?: string;
scaleStep?: number;
minScale?: number;
maxScale?: number;
motionName?: string;
open?: boolean;
getContainer?: PortalProps['getContainer'];
zIndex?: number;
maskClosable?: boolean;
afterOpenChange?: (open: boolean) => void;
/** Whether to trap focus within the preview when open. Default is true. */
focusTrap?: boolean;
movable?: boolean;
icons?: OperationIcons;
closeIcon?: React.ReactNode;
onTransform?: (info: {
transform: TransformType;
action: TransformAction;
}) => void;
countRender?: (current: number, total: number) => React.ReactNode;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
current?: number;
image: ImgInfo;
}) => React.ReactNode;
actionsRender?: (originalNode: React.ReactElement, info: ToolbarRenderInfoType) => React.ReactNode;
}
export interface PreviewProps extends InternalPreviewConfig {
prefixCls: string;
classNames?: Partial<Record<PreviewSemanticName, string>>;
styles?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
imageInfo?: {
width: number | string;
height: number | string;
};
fallback?: string;
imgCommonProps?: React.ImgHTMLAttributes<HTMLImageElement>;
width?: string | number;
height?: string | number;
current?: number;
count?: number;
onChange?: (current: number, prev: number) => void;
onClose?: () => void;
mousePosition: null | {
x: number;
y: number;
};
}
declare const Preview: React.FC<PreviewProps>;
export default Preview;
+363
View File
@@ -0,0 +1,363 @@
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;
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react';
import type { ImgInfo } from './Image';
import type { InternalPreviewConfig, PreviewProps, PreviewSemanticName } from './Preview';
import type { TransformType } from './hooks/useImageTransform';
import type { ImageElementProps } from './interface';
export interface GroupPreviewConfig extends InternalPreviewConfig {
current?: number;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
current: number;
image: ImgInfo;
}) => React.ReactNode;
onOpenChange?: (value: boolean, info: {
current: number;
}) => void;
onChange?: (current: number, prevCurrent: number) => void;
}
export interface PreviewGroupProps {
previewPrefixCls?: string;
classNames?: {
popup?: Partial<Record<PreviewSemanticName, string>>;
};
styles?: {
popup?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
};
icons?: PreviewProps['icons'];
items?: (string | ImageElementProps)[];
fallback?: string;
preview?: boolean | GroupPreviewConfig;
children?: React.ReactNode;
}
declare const Group: React.FC<PreviewGroupProps>;
export default Group;
+112
View File
@@ -0,0 +1,112 @@
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 useControlledState from "@rc-component/util/es/hooks/useControlledState";
import useEvent from "@rc-component/util/es/hooks/useEvent";
import * as React from 'react';
import { useState } from 'react';
import Preview from "./Preview";
import { PreviewGroupContext } from "./context";
import usePreviewItems from "./hooks/usePreviewItems";
const Group = ({
previewPrefixCls = 'rc-image-preview',
classNames,
styles,
children,
icons = {},
items,
preview,
fallback
}) => {
const {
open: previewOpen,
onOpenChange,
current: currentIndex,
onChange,
...restProps
} = preview && typeof preview === 'object' ? preview : {};
// ========================== Items ===========================
const [mergedItems, register, fromItems] = usePreviewItems(items);
// ========================= Preview ==========================
// >>> Index
const [current, setCurrent] = useControlledState(0, currentIndex);
const [keepOpenIndex, setKeepOpenIndex] = useState(false);
// >>> Image
const {
src,
...imgCommonProps
} = mergedItems[current]?.data || {};
// >>> Visible
const [isShowPreview, setShowPreview] = useControlledState(!!previewOpen, previewOpen);
const triggerShowPreview = useEvent(next => {
setShowPreview(next);
if (next !== isShowPreview) {
onOpenChange?.(next, {
current
});
}
});
// >>> Position
const [mousePosition, setMousePosition] = useState(null);
const onPreviewFromImage = React.useCallback((id, imageSrc, mouseX, mouseY) => {
const index = fromItems ? mergedItems.findIndex(item => item.data.src === imageSrc) : mergedItems.findIndex(item => item.id === id);
setCurrent(index < 0 ? 0 : index);
triggerShowPreview(true);
setMousePosition({
x: mouseX,
y: mouseY
});
setKeepOpenIndex(true);
}, [mergedItems, fromItems]);
// Reset current when reopen
React.useEffect(() => {
if (isShowPreview) {
if (!keepOpenIndex) {
setCurrent(0);
}
} else {
setKeepOpenIndex(false);
}
}, [isShowPreview]);
// ========================== Events ==========================
const onInternalChange = (next, prev) => {
setCurrent(next);
onChange?.(next, prev);
};
const onPreviewClose = () => {
triggerShowPreview(false);
setMousePosition(null);
};
// ========================= Context ==========================
const previewGroupContext = React.useMemo(() => ({
register,
onPreview: onPreviewFromImage
}), [register, onPreviewFromImage]);
// ========================== Render ==========================
return /*#__PURE__*/React.createElement(PreviewGroupContext.Provider, {
value: previewGroupContext
}, children, /*#__PURE__*/React.createElement(Preview, _extends({
"aria-hidden": !isShowPreview,
open: isShowPreview,
prefixCls: previewPrefixCls,
onClose: onPreviewClose,
mousePosition: mousePosition,
imgCommonProps: imgCommonProps,
src: src,
fallback: fallback,
icons: icons,
current: current,
count: mergedItems.length,
onChange: onInternalChange
}, restProps, {
classNames: classNames?.popup,
styles: styles?.popup
})));
};
export default Group;
+2
View File
@@ -0,0 +1,2 @@
import type { ImageElementProps } from './interface';
export declare const COMMON_PROPS: (keyof Omit<ImageElementProps, 'src'>)[];
+1
View File
@@ -0,0 +1 @@
export const COMMON_PROPS = ['crossOrigin', 'decoding', 'draggable', 'loading', 'referrerPolicy', 'sizes', 'srcSet', 'useMap', 'alt', 'fetchPriority'];
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react';
import type { OnGroupPreview, RegisterImage } from './interface';
export interface PreviewGroupContextProps {
register: RegisterImage;
onPreview: OnGroupPreview;
}
export declare const PreviewGroupContext: React.Context<PreviewGroupContextProps>;
+2
View File
@@ -0,0 +1,2 @@
import * as React from 'react';
export const PreviewGroupContext = /*#__PURE__*/React.createContext(null);
@@ -0,0 +1,16 @@
/**
* Fix positon x,y point when
*
* Ele width && height < client
* - Back origin
*
* - Ele width | height > clientWidth | clientHeight
* - left | top > 0 -> Back 0
* - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight
*
* Regardless of other
*/
export default function getFixScaleEleTransPosition(width: number, height: number, left: number, top: number): null | {
x: number;
y: number;
};
@@ -0,0 +1,54 @@
import { getClientSize } from "./util";
function fixPoint(key, start, width, clientWidth) {
const startAddWidth = start + width;
const offsetStart = (width - clientWidth) / 2;
if (width > clientWidth) {
if (start > 0) {
return {
[key]: offsetStart
};
}
if (start < 0 && startAddWidth < clientWidth) {
return {
[key]: -offsetStart
};
}
} else if (start < 0 || startAddWidth > clientWidth) {
return {
[key]: start < 0 ? offsetStart : -offsetStart
};
}
return {};
}
/**
* Fix positon x,y point when
*
* Ele width && height < client
* - Back origin
*
* - Ele width | height > clientWidth | clientHeight
* - left | top > 0 -> Back 0
* - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight
*
* Regardless of other
*/
export default function getFixScaleEleTransPosition(width, height, left, top) {
const {
width: clientWidth,
height: clientHeight
} = getClientSize();
let fixPos = null;
if (width <= clientWidth && height <= clientHeight) {
fixPos = {
x: 0,
y: 0
};
} else if (width > clientWidth || height > clientHeight) {
fixPos = {
...fixPoint('x', left, width, clientWidth),
...fixPoint('y', top, height, clientHeight)
};
}
return fixPos;
}
@@ -0,0 +1,28 @@
/// <reference types="react" />
export type TransformType = {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
};
export type TransformAction = 'flipY' | 'flipX' | 'rotateLeft' | 'rotateRight' | 'zoomIn' | 'zoomOut' | 'close' | 'prev' | 'next' | 'wheel' | 'doubleClick' | 'move' | 'dragRebound' | 'touchZoom' | 'reset';
export type UpdateTransformFunc = (newTransform: Partial<TransformType>, action: TransformAction) => void;
export type DispatchZoomChangeFunc = (ratio: number, action: TransformAction, centerX?: number, centerY?: number, isTouch?: boolean) => void;
export default function useImageTransform(imgRef: React.MutableRefObject<HTMLImageElement>, minScale: number, maxScale: number, onTransform: (info: {
transform: TransformType;
action: TransformAction;
}) => void): {
transform: {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
};
resetTransform: (action: TransformAction) => void;
updateTransform: UpdateTransformFunc;
dispatchZoomChange: DispatchZoomChangeFunc;
};
@@ -0,0 +1,118 @@
import { getClientSize } from "../util";
import isEqual from "@rc-component/util/es/isEqual";
import raf from "@rc-component/util/es/raf";
import { useRef, useState } from 'react';
const initialTransform = {
x: 0,
y: 0,
rotate: 0,
scale: 1,
flipX: false,
flipY: false
};
export default function useImageTransform(imgRef, minScale, maxScale, onTransform) {
const frame = useRef(null);
const queue = useRef([]);
const [transform, setTransform] = useState(initialTransform);
const resetTransform = action => {
setTransform(initialTransform);
if (!isEqual(initialTransform, transform)) {
onTransform?.({
transform: initialTransform,
action
});
}
};
/** Direct update transform */
const updateTransform = (newTransform, action) => {
if (frame.current === null) {
queue.current = [];
frame.current = raf(() => {
setTransform(preState => {
let memoState = preState;
queue.current.forEach(queueState => {
memoState = {
...memoState,
...queueState
};
});
frame.current = null;
onTransform?.({
transform: memoState,
action
});
return memoState;
});
});
}
queue.current.push({
...transform,
...newTransform
});
};
/** Scale according to the position of centerX and centerY */
const dispatchZoomChange = (ratio, action, centerX, centerY, isTouch) => {
const {
width,
height,
offsetWidth,
offsetHeight,
offsetLeft,
offsetTop
} = imgRef.current;
let newRatio = ratio;
let newScale = transform.scale * ratio;
if (newScale > maxScale) {
newScale = maxScale;
newRatio = maxScale / transform.scale;
} else if (newScale < minScale) {
// For mobile interactions, allow scaling down to the minimum scale.
newScale = isTouch ? newScale : minScale;
newRatio = newScale / transform.scale;
}
/** Default center point scaling */
const mergedCenterX = centerX ?? innerWidth / 2;
const mergedCenterY = centerY ?? innerHeight / 2;
const diffRatio = newRatio - 1;
/** Deviation calculated from image size */
const diffImgX = diffRatio * width * 0.5;
const diffImgY = diffRatio * height * 0.5;
/** The difference between the click position and the edge of the document */
const diffOffsetLeft = diffRatio * (mergedCenterX - transform.x - offsetLeft);
const diffOffsetTop = diffRatio * (mergedCenterY - transform.y - offsetTop);
/** Final positioning */
let newX = transform.x - (diffOffsetLeft - diffImgX);
let newY = transform.y - (diffOffsetTop - diffImgY);
/**
* When zooming the image
* When the image size is smaller than the width and height of the window, the position is initialized
*/
if (ratio < 1 && newScale === 1) {
const mergedWidth = offsetWidth * newScale;
const mergedHeight = offsetHeight * newScale;
const {
width: clientWidth,
height: clientHeight
} = getClientSize();
if (mergedWidth <= clientWidth && mergedHeight <= clientHeight) {
newX = 0;
newY = 0;
}
}
updateTransform({
x: newX,
y: newY,
scale: newScale
}, action);
};
return {
transform,
resetTransform,
updateTransform,
dispatchZoomChange
};
}
@@ -0,0 +1,9 @@
import type React from 'react';
import type { DispatchZoomChangeFunc, TransformType, UpdateTransformFunc } from './useImageTransform';
export default function useMouseEvent(imgRef: React.MutableRefObject<HTMLImageElement>, movable: boolean, open: boolean, scaleStep: number, transform: TransformType, updateTransform: UpdateTransformFunc, dispatchZoomChange: DispatchZoomChangeFunc): {
isMoving: boolean;
onMouseDown: React.MouseEventHandler<HTMLDivElement>;
onMouseMove: (event: MouseEvent) => void;
onMouseUp: () => void;
onWheel: (event: React.WheelEvent<HTMLImageElement>) => void;
};
@@ -0,0 +1,116 @@
import { warning } from "@rc-component/util/es/warning";
import { useEffect, useRef, useState } from 'react';
import getFixScaleEleTransPosition from "../getFixScaleEleTransPosition";
import { BASE_SCALE_RATIO, WHEEL_MAX_SCALE_RATIO } from "../previewConfig";
export default function useMouseEvent(imgRef, movable, open, scaleStep, transform, updateTransform, dispatchZoomChange) {
const {
rotate,
scale,
x,
y
} = transform;
const [isMoving, setMoving] = useState(false);
const startPositionInfo = useRef({
diffX: 0,
diffY: 0,
transformX: 0,
transformY: 0
});
const onMouseDown = event => {
// Only allow main button
if (!movable || event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
startPositionInfo.current = {
diffX: event.pageX - x,
diffY: event.pageY - y,
transformX: x,
transformY: y
};
setMoving(true);
};
const onMouseMove = event => {
if (open && isMoving) {
updateTransform({
x: event.pageX - startPositionInfo.current.diffX,
y: event.pageY - startPositionInfo.current.diffY
}, 'move');
}
};
const onMouseUp = () => {
if (open && isMoving) {
setMoving(false);
/** No need to restore the position when the picture is not moved, So as not to interfere with the click */
const {
transformX,
transformY
} = startPositionInfo.current;
const hasChangedPosition = x !== transformX && y !== transformY;
if (!hasChangedPosition) return;
const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
const {
left,
top
} = imgRef.current.getBoundingClientRect();
const isRotate = rotate % 180 !== 0;
const fixState = getFixScaleEleTransPosition(isRotate ? height : width, isRotate ? width : height, left, top);
if (fixState) {
updateTransform({
...fixState
}, 'dragRebound');
}
}
};
const onWheel = event => {
if (!open || event.deltaY == 0) return;
// Scale ratio depends on the deltaY size
const scaleRatio = Math.abs(event.deltaY / 100);
// Limit the maximum scale ratio
const mergedScaleRatio = Math.min(scaleRatio, WHEEL_MAX_SCALE_RATIO);
// Scale the ratio each time
let ratio = BASE_SCALE_RATIO + mergedScaleRatio * scaleStep;
if (event.deltaY > 0) {
ratio = BASE_SCALE_RATIO / ratio;
}
dispatchZoomChange(ratio, 'wheel', event.clientX, event.clientY);
};
useEffect(() => {
if (movable) {
window.addEventListener('mouseup', onMouseUp, false);
window.addEventListener('mousemove', onMouseMove, false);
try {
// Resolve if in iframe lost event
/* istanbul ignore next */
if (window.top !== window.self) {
window.top.addEventListener('mouseup', onMouseUp, false);
window.top.addEventListener('mousemove', onMouseMove, false);
}
} catch (error) {
/* istanbul ignore next */
warning(false, `[rc-image] ${error}`);
}
}
return () => {
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('mousemove', onMouseMove);
/* istanbul ignore next */
try {
window.top?.removeEventListener('mouseup', onMouseUp);
window.top?.removeEventListener('mousemove', onMouseMove);
} catch (error) {
// Do nothing
}
};
}, [open, isMoving, x, y, rotate, movable]);
return {
isMoving,
onMouseDown,
onMouseMove,
onMouseUp,
onWheel
};
}
@@ -0,0 +1,7 @@
import type { PreviewGroupProps } from '../PreviewGroup';
import type { InternalItem, RegisterImage } from '../interface';
export type Items = Omit<InternalItem, 'canPreview'>[];
/**
* Merge props provided `items` or context collected images
*/
export default function usePreviewItems(items?: PreviewGroupProps['items']): [items: Items, registerImage: RegisterImage, fromItems: boolean];
@@ -0,0 +1,65 @@
import * as React from 'react';
import { COMMON_PROPS } from "../common";
/**
* Merge props provided `items` or context collected images
*/
export default function usePreviewItems(items) {
// Context collection image data
const [images, setImages] = React.useState({});
const registerImage = React.useCallback((id, data) => {
setImages(imgs => ({
...imgs,
[id]: data
}));
return () => {
setImages(imgs => {
const cloneImgs = {
...imgs
};
delete cloneImgs[id];
return cloneImgs;
});
};
}, []);
// items
const mergedItems = React.useMemo(() => {
// use `items` first
if (items) {
return items.map(item => {
if (typeof item === 'string') {
return {
data: {
src: item
}
};
}
const data = {};
Object.keys(item).forEach(key => {
if (['src', ...COMMON_PROPS].includes(key)) {
data[key] = item[key];
}
});
return {
data
};
});
}
// use registered images secondly
return Object.keys(images).reduce((total, id) => {
const {
canPreview,
data
} = images[id];
if (canPreview) {
total.push({
data,
id
});
}
return total;
}, []);
}, [items, images]);
return [mergedItems, registerImage, !!items];
}
@@ -0,0 +1,2 @@
import type { ImageElementProps } from '../interface';
export default function useRegisterImage(canPreview: boolean, data: ImageElementProps): string;
@@ -0,0 +1,29 @@
import * as React from 'react';
import { PreviewGroupContext } from "../context";
let uid = 0;
export default function useRegisterImage(canPreview, data) {
const [id] = React.useState(() => {
uid += 1;
return String(uid);
});
const groupContext = React.useContext(PreviewGroupContext);
const registerData = {
data,
canPreview
};
// Keep order start
// Resolve https://github.com/ant-design/ant-design/issues/28881
// Only need unRegister when component unMount
React.useEffect(() => {
if (groupContext) {
return groupContext.register(id, registerData);
}
}, []);
React.useEffect(() => {
if (groupContext) {
groupContext.register(id, registerData);
}
}, [canPreview, data]);
return id;
}
@@ -0,0 +1,13 @@
type ImageStatus = 'normal' | 'error' | 'loading';
export default function useStatus({ src, isCustomPlaceholder, fallback, }: {
src: string;
isCustomPlaceholder?: boolean;
fallback?: string;
}): readonly [(img?: HTMLImageElement) => void, {
src: string;
onLoad?: undefined;
} | {
onLoad: () => void;
src: string;
}, ImageStatus];
export {};
+50
View File
@@ -0,0 +1,50 @@
import { useEffect, useRef, useState } from 'react';
import { isImageValid } from "../util";
export default function useStatus({
src,
isCustomPlaceholder,
fallback
}) {
const [status, setStatus] = useState(isCustomPlaceholder ? 'loading' : 'normal');
const isLoaded = useRef(false);
const isError = status === 'error';
// https://github.com/react-component/image/pull/187
useEffect(() => {
let isCurrentSrc = true;
isImageValid(src).then(isValid => {
// https://github.com/ant-design/ant-design/issues/44948
// If src changes, the previous setStatus should not be triggered
if (!isValid && isCurrentSrc) {
setStatus('error');
}
});
return () => {
isCurrentSrc = false;
};
}, [src]);
useEffect(() => {
if (isCustomPlaceholder && !isLoaded.current) {
setStatus('loading');
} else if (isError) {
setStatus('normal');
}
}, [src]);
const onLoad = () => {
setStatus('normal');
};
const getImgRef = img => {
isLoaded.current = false;
if (status === 'loading' && img?.complete && (img.naturalWidth || img.naturalHeight)) {
isLoaded.current = true;
onLoad();
}
};
const srcAndOnload = isError && fallback ? {
src: fallback
} : {
onLoad,
src
};
return [getImgRef, srcAndOnload, status];
}
@@ -0,0 +1,8 @@
import type React from 'react';
import type { DispatchZoomChangeFunc, TransformType, UpdateTransformFunc } from './useImageTransform';
export default function useTouchEvent(imgRef: React.MutableRefObject<HTMLImageElement>, movable: boolean, open: boolean, minScale: number, transform: TransformType, updateTransform: UpdateTransformFunc, dispatchZoomChange: DispatchZoomChangeFunc): {
isTouching: boolean;
onTouchStart: (event: React.TouchEvent<HTMLImageElement>) => void;
onTouchMove: (event: React.TouchEvent<HTMLImageElement>) => void;
onTouchEnd: () => void;
};
@@ -0,0 +1,170 @@
import { useEffect, useRef, useState } from 'react';
import getFixScaleEleTransPosition from "../getFixScaleEleTransPosition";
function getDistance(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.hypot(x, y);
}
function getCenter(oldPoint1, oldPoint2, newPoint1, newPoint2) {
// Calculate the distance each point has moved
const distance1 = getDistance(oldPoint1, newPoint1);
const distance2 = getDistance(oldPoint2, newPoint2);
// If both distances are 0, return the original points
if (distance1 === 0 && distance2 === 0) {
return [oldPoint1.x, oldPoint1.y];
}
// Calculate the ratio of the distances
const ratio = distance1 / (distance1 + distance2);
// Calculate the new center point based on the ratio
const x = oldPoint1.x + ratio * (oldPoint2.x - oldPoint1.x);
const y = oldPoint1.y + ratio * (oldPoint2.y - oldPoint1.y);
return [x, y];
}
export default function useTouchEvent(imgRef, movable, open, minScale, transform, updateTransform, dispatchZoomChange) {
const {
rotate,
scale,
x,
y
} = transform;
const [isTouching, setIsTouching] = useState(false);
const touchPointInfo = useRef({
point1: {
x: 0,
y: 0
},
point2: {
x: 0,
y: 0
},
eventType: 'none'
});
const updateTouchPointInfo = values => {
touchPointInfo.current = {
...touchPointInfo.current,
...values
};
};
const onTouchStart = event => {
if (!movable) return;
event.stopPropagation();
setIsTouching(true);
const {
touches = []
} = event;
if (touches.length > 1) {
// touch zoom
updateTouchPointInfo({
point1: {
x: touches[0].clientX,
y: touches[0].clientY
},
point2: {
x: touches[1].clientX,
y: touches[1].clientY
},
eventType: 'touchZoom'
});
} else {
// touch move
updateTouchPointInfo({
point1: {
x: touches[0].clientX - x,
y: touches[0].clientY - y
},
eventType: 'move'
});
}
};
const onTouchMove = event => {
const {
touches = []
} = event;
const {
point1,
point2,
eventType
} = touchPointInfo.current;
if (touches.length > 1 && eventType === 'touchZoom') {
// touch zoom
const newPoint1 = {
x: touches[0].clientX,
y: touches[0].clientY
};
const newPoint2 = {
x: touches[1].clientX,
y: touches[1].clientY
};
const [centerX, centerY] = getCenter(point1, point2, newPoint1, newPoint2);
const ratio = getDistance(newPoint1, newPoint2) / getDistance(point1, point2);
dispatchZoomChange(ratio, 'touchZoom', centerX, centerY, true);
updateTouchPointInfo({
point1: newPoint1,
point2: newPoint2,
eventType: 'touchZoom'
});
} else if (eventType === 'move') {
// touch move
updateTransform({
x: touches[0].clientX - point1.x,
y: touches[0].clientY - point1.y
}, 'move');
updateTouchPointInfo({
eventType: 'move'
});
}
};
const onTouchEnd = () => {
if (!open) return;
if (isTouching) {
setIsTouching(false);
}
updateTouchPointInfo({
eventType: 'none'
});
if (minScale > scale) {
/** When the scaling ratio is less than the minimum scaling ratio, reset the scaling ratio */
return updateTransform({
x: 0,
y: 0,
scale: minScale
}, 'touchZoom');
}
const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
const {
left,
top
} = imgRef.current.getBoundingClientRect();
const isRotate = rotate % 180 !== 0;
const fixState = getFixScaleEleTransPosition(isRotate ? height : width, isRotate ? width : height, left, top);
if (fixState) {
updateTransform({
...fixState
}, 'dragRebound');
}
};
useEffect(() => {
const preventDefault = e => {
e.preventDefault();
};
if (open && movable) {
window.addEventListener('touchmove', preventDefault, {
passive: false
});
}
return () => {
window.removeEventListener('touchmove', preventDefault);
};
}, [open, movable]);
return {
isTouching,
onTouchStart,
onTouchMove,
onTouchEnd
};
}
+3
View File
@@ -0,0 +1,3 @@
import Image from './Image';
export * from './Image';
export default Image;
+3
View File
@@ -0,0 +1,3 @@
import Image from "./Image";
export * from "./Image";
export default Image;
+14
View File
@@ -0,0 +1,14 @@
/// <reference types="react" />
/**
* Used for PreviewGroup passed image data
*/
export type ImageElementProps = Pick<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'crossOrigin' | 'decoding' | 'draggable' | 'loading' | 'referrerPolicy' | 'sizes' | 'srcSet' | 'useMap' | 'alt' | 'fetchPriority'>;
export type PreviewImageElementProps = {
data: ImageElementProps;
canPreview: boolean;
};
export type InternalItem = PreviewImageElementProps & {
id?: string;
};
export type RegisterImage = (id: string, data: PreviewImageElementProps) => VoidFunction;
export type OnGroupPreview = (id: string, imageSrc: string, mouseX: number, mouseY: number) => void;
+1
View File
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,4 @@
/** Scale the ratio base */
export declare const BASE_SCALE_RATIO = 1;
/** The maximum zoom ratio when the mouse zooms in, adjustable */
export declare const WHEEL_MAX_SCALE_RATIO = 1;
+4
View File
@@ -0,0 +1,4 @@
/** Scale the ratio base */
export const BASE_SCALE_RATIO = 1;
/** The maximum zoom ratio when the mouse zooms in, adjustable */
export const WHEEL_MAX_SCALE_RATIO = 1;
+5
View File
@@ -0,0 +1,5 @@
export declare function isImageValid(src: string): Promise<unknown>;
export declare function getClientSize(): {
width: number;
height: number;
};
+22
View File
@@ -0,0 +1,22 @@
export function isImageValid(src) {
return new Promise(resolve => {
if (!src) {
resolve(false);
return;
}
const img = document.createElement('img');
img.onerror = () => resolve(false);
img.onload = () => resolve(true);
img.src = src;
});
}
// ============================= Legacy =============================
export function getClientSize() {
const width = document.documentElement.clientWidth;
const height = window.innerHeight || document.documentElement.clientHeight;
return {
width,
height
};
}
+47
View File
@@ -0,0 +1,47 @@
import * as React from 'react';
import type { InternalPreviewConfig, PreviewSemanticName, ToolbarRenderInfoType } from './Preview';
import PreviewGroup from './PreviewGroup';
import type { TransformType } from './hooks/useImageTransform';
export interface ImgInfo {
url: string;
alt: string;
width: string | number;
height: string | number;
}
export interface CoverConfig {
coverNode?: React.ReactNode;
placement?: 'top' | 'bottom' | 'center';
}
export interface PreviewConfig extends Omit<InternalPreviewConfig, 'countRender'> {
cover?: React.ReactNode | CoverConfig;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
image: ImgInfo;
}) => React.ReactNode;
actionsRender?: (originalNode: React.ReactElement, info: Omit<ToolbarRenderInfoType, 'current' | 'total'>) => React.ReactNode;
onOpenChange?: (open: boolean) => void;
}
export type SemanticName = 'root' | 'image' | 'cover';
export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'placeholder' | 'onClick' | 'onKeyDown'> {
prefixCls?: string;
previewPrefixCls?: string;
rootClassName?: string;
classNames?: Partial<Record<SemanticName, string> & {
popup?: Partial<Record<PreviewSemanticName, string>>;
}>;
styles?: Partial<Record<SemanticName, React.CSSProperties> & {
popup?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
}>;
src?: string;
placeholder?: React.ReactNode;
fallback?: string;
preview?: boolean | PreviewConfig;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
}
interface CompoundedComponent<P> extends React.FC<P> {
PreviewGroup: typeof PreviewGroup;
}
declare const ImageInternal: CompoundedComponent<ImageProps>;
export default ImageInternal;
+200
View File
@@ -0,0 +1,200 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
var _clsx = require("clsx");
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _Preview = _interopRequireDefault(require("./Preview"));
var _PreviewGroup = _interopRequireDefault(require("./PreviewGroup"));
var _common = require("./common");
var _context = require("./context");
var _useRegisterImage = _interopRequireDefault(require("./hooks/useRegisterImage"));
var _useStatus = _interopRequireDefault(require("./hooks/useStatus"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
const ImageInternal = props => {
const {
// Misc
prefixCls = 'rc-image',
previewPrefixCls = `${prefixCls}-preview`,
// Style
rootClassName,
className,
style,
classNames = {},
styles = {},
width,
height,
// Image
src: imgSrc,
alt,
placeholder,
fallback,
// Preview
preview = true,
// Events
onClick,
onError,
onKeyDown,
...otherProps
} = props;
const groupContext = (0, _react.useContext)(_context.PreviewGroupContext);
// ========================== Preview ===========================
const canPreview = !!preview;
const {
src: previewSrc,
open: previewOpen,
onOpenChange: onPreviewOpenChange,
cover,
rootClassName: previewRootClassName,
...restProps
} = preview && typeof preview === 'object' ? preview : {};
const coverPlacement = typeof cover === 'object' && cover.placement ? cover.placement || 'center' : 'center';
const coverNode = typeof cover === 'object' && cover.coverNode ? cover.coverNode : cover;
// ============================ Open ============================
const [isShowPreview, setShowPreview] = (0, _useControlledState.default)(!!previewOpen, previewOpen);
const [mousePosition, setMousePosition] = (0, _react.useState)(null);
const triggerPreviewOpen = nextOpen => {
setShowPreview(nextOpen);
onPreviewOpenChange?.(nextOpen);
};
const onPreviewClose = () => {
triggerPreviewOpen(false);
};
// ========================= ImageProps =========================
const isCustomPlaceholder = placeholder && placeholder !== true;
const src = previewSrc ?? imgSrc;
const [getImgRef, srcAndOnload, status] = (0, _useStatus.default)({
src: imgSrc,
isCustomPlaceholder,
fallback
});
const imgCommonProps = (0, _react.useMemo)(() => {
const obj = {};
_common.COMMON_PROPS.forEach(prop => {
if (props[prop] !== undefined) {
obj[prop] = props[prop];
}
});
return obj;
}, _common.COMMON_PROPS.map(prop => props[prop]));
// ========================== Register ==========================
const registerData = (0, _react.useMemo)(() => ({
...imgCommonProps,
src
}), [src, imgCommonProps]);
const imageId = (0, _useRegisterImage.default)(canPreview, registerData);
// ========================== Preview ===========================
const onPreview = e => {
const rect = e.target.getBoundingClientRect();
const left = rect.x + rect.width / 2;
const top = rect.y + rect.height / 2;
if (groupContext) {
groupContext.onPreview(imageId, src, left, top);
} else {
setMousePosition({
x: left,
y: top
});
triggerPreviewOpen(true);
}
onClick?.(e);
};
// ======================= Keyboard Preview =====================
const onPreviewKeyDown = event => {
onKeyDown?.(event);
if (!canPreview) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
const rect = event.target.getBoundingClientRect();
const left = rect.x + rect.width / 2;
const top = rect.y + rect.height / 2;
if (groupContext) {
groupContext.onPreview(imageId, src, left, top);
} else {
setMousePosition({
x: left,
y: top
});
triggerPreviewOpen(true);
}
}
};
// =========================== Render ===========================
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", _extends({}, otherProps, {
className: (0, _clsx.clsx)(prefixCls, rootClassName, classNames.root, {
[`${prefixCls}-error`]: status === 'error'
}),
onClick: canPreview ? onPreview : onClick,
role: canPreview ? 'button' : otherProps.role,
tabIndex: canPreview && otherProps.tabIndex == null ? 0 : otherProps.tabIndex,
"aria-label": canPreview ? otherProps['aria-label'] ?? alt : otherProps['aria-label'],
onKeyDown: onPreviewKeyDown,
style: {
width,
height,
...styles.root
}
}), /*#__PURE__*/React.createElement("img", _extends({}, imgCommonProps, {
className: (0, _clsx.clsx)(`${prefixCls}-img`, {
[`${prefixCls}-img-placeholder`]: placeholder === true
}, classNames.image, className),
style: {
height,
...styles.image,
...style
},
ref: getImgRef
}, srcAndOnload, {
width: width,
height: height,
onError: onError
})), status === 'loading' && /*#__PURE__*/React.createElement("div", {
"aria-hidden": "true",
className: `${prefixCls}-placeholder`
}, placeholder), cover !== false && canPreview && /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-cover`, classNames.cover, `${prefixCls}-cover-${coverPlacement}`),
style: {
display: style?.display === 'none' ? 'none' : undefined,
...styles.cover
}
}, coverNode)), !groupContext && canPreview && /*#__PURE__*/React.createElement(_Preview.default, _extends({
"aria-hidden": !isShowPreview,
open: isShowPreview,
prefixCls: previewPrefixCls,
onClose: onPreviewClose,
mousePosition: mousePosition,
src: src,
alt: alt,
imageInfo: {
width,
height
},
fallback: fallback,
imgCommonProps: imgCommonProps
}, restProps, {
classNames: classNames?.popup,
styles: styles?.popup,
rootClassName: (0, _clsx.clsx)(previewRootClassName, rootClassName)
})));
};
ImageInternal.PreviewGroup = _PreviewGroup.default;
if (process.env.NODE_ENV !== 'production') {
ImageInternal.displayName = 'Image';
}
var _default = exports.default = ImageInternal;
@@ -0,0 +1,9 @@
import * as React from 'react';
export interface CloseBtnProps {
prefixCls: string;
icon?: React.ReactNode;
onClick: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
}
export default function CloseBtn(props: CloseBtnProps): React.JSX.Element;
@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = CloseBtn;
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function CloseBtn(props) {
const {
prefixCls,
icon,
onClick,
className,
style
} = props;
return /*#__PURE__*/React.createElement("button", {
className: (0, _clsx.clsx)(`${prefixCls}-close`, className),
style: style,
onClick: onClick
}, icon);
}
@@ -0,0 +1,23 @@
import * as React from 'react';
import type { Actions, PreviewProps } from '.';
import type { ImgInfo } from '../Image';
import type { TransformType } from '../hooks/useImageTransform';
export type FooterSemanticName = 'footer' | 'actions';
export interface FooterProps extends Actions {
prefixCls: string;
showProgress: boolean;
countRender?: PreviewProps['countRender'];
actionsRender?: PreviewProps['actionsRender'];
current: number;
count: number;
showSwitch: boolean;
icons: PreviewProps['icons'];
scale: number;
minScale: number;
maxScale: number;
image: ImgInfo;
transform: TransformType;
classNames: Partial<Record<FooterSemanticName, string>>;
styles: Partial<Record<FooterSemanticName, React.CSSProperties>>;
}
export default function Footer(props: FooterProps): React.JSX.Element;
+161
View File
@@ -0,0 +1,161 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Footer;
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function Footer(props) {
// 修改解构,添加缺失的属性,并提供默认值
const {
prefixCls,
showProgress,
current,
count,
showSwitch,
// Style
classNames,
styles,
// render
icons,
image,
transform,
countRender,
actionsRender,
// Scale
scale,
minScale,
maxScale,
// Actions
onActive,
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onClose,
onReset
} = props;
const {
left,
right,
prev,
next,
flipY,
flipX,
rotateLeft,
rotateRight,
zoomOut,
zoomIn
} = icons;
// ========================== Render ==========================
// >>>>> Progress
const progressNode = showProgress && /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-progress`
}, countRender ? countRender(current + 1, count) : /*#__PURE__*/React.createElement("bdi", null, `${current + 1} / ${count}`));
// >>>>> Actions
const actionCls = `${prefixCls}-actions-action`;
const renderOperation = ({
type,
disabled,
onClick,
icon
}) => {
return /*#__PURE__*/React.createElement("button", {
type: "button",
key: type,
className: (0, _clsx.clsx)(actionCls, `${actionCls}-${type}`, {
[`${actionCls}-disabled`]: !!disabled
}),
onClick: onClick,
disabled: !!disabled,
"aria-label": type
}, icon);
};
const switchPrevNode = showSwitch ? renderOperation({
icon: prev ?? left,
onClick: () => onActive(-1),
type: 'prev',
disabled: current === 0
}) : undefined;
const switchNextNode = showSwitch ? renderOperation({
icon: next ?? right,
onClick: () => onActive(1),
type: 'next',
disabled: current === count - 1
}) : undefined;
const flipYNode = renderOperation({
icon: flipY,
onClick: onFlipY,
type: 'flipY'
});
const flipXNode = renderOperation({
icon: flipX,
onClick: onFlipX,
type: 'flipX'
});
const rotateLeftNode = renderOperation({
icon: rotateLeft,
onClick: onRotateLeft,
type: 'rotateLeft'
});
const rotateRightNode = renderOperation({
icon: rotateRight,
onClick: onRotateRight,
type: 'rotateRight'
});
const zoomOutNode = renderOperation({
icon: zoomOut,
onClick: onZoomOut,
type: 'zoomOut',
disabled: scale <= minScale
});
const zoomInNode = renderOperation({
icon: zoomIn,
onClick: onZoomIn,
type: 'zoomIn',
disabled: scale === maxScale
});
const actionsNode = /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-actions`, classNames.actions),
style: styles.actions
}, flipYNode, flipXNode, rotateLeftNode, rotateRightNode, zoomOutNode, zoomInNode);
// >>>>> Render
return /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-footer`, classNames.footer),
style: styles.footer
}, progressNode, actionsRender ? actionsRender(actionsNode, {
icons: {
prevIcon: switchPrevNode,
nextIcon: switchNextNode,
flipYIcon: flipYNode,
flipXIcon: flipXNode,
rotateLeftIcon: rotateLeftNode,
rotateRightIcon: rotateRightNode,
zoomOutIcon: zoomOutNode,
zoomInIcon: zoomInNode
},
actions: {
onActive,
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onReset,
onClose
},
transform,
current,
total: count,
image
}) : actionsNode);
}
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { OperationIcons } from '.';
export interface PrevNextProps {
prefixCls: string;
onActive: (offset: number) => void;
current: number;
count: number;
icons: OperationIcons;
}
export default function PrevNext(props: PrevNextProps): React.JSX.Element;
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = PrevNext;
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function PrevNext(props) {
const {
prefixCls,
onActive,
current,
count,
icons: {
left,
right,
prev,
next
}
} = props;
const switchCls = `${prefixCls}-switch`;
const prevDisabled = current === 0;
const nextDisabled = current === count - 1;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
className: (0, _clsx.clsx)(switchCls, `${switchCls}-prev`, {
[`${switchCls}-disabled`]: prevDisabled
}),
onClick: () => onActive(-1),
disabled: prevDisabled
}, prev ?? left), /*#__PURE__*/React.createElement("button", {
type: "button",
className: (0, _clsx.clsx)(switchCls, `${switchCls}-next`, {
[`${switchCls}-disabled`]: nextDisabled
}),
onClick: () => onActive(1),
disabled: nextDisabled
}, next ?? right));
}
+103
View File
@@ -0,0 +1,103 @@
import { type PortalProps } from '@rc-component/portal';
import React from 'react';
import type { TransformAction, TransformType } from '../hooks/useImageTransform';
import type { ImgInfo } from '../Image';
import { type FooterSemanticName } from './Footer';
export type PreviewSemanticName = 'root' | 'mask' | 'body' | 'close' | FooterSemanticName;
export interface OperationIcons {
rotateLeft?: React.ReactNode;
rotateRight?: React.ReactNode;
zoomIn?: React.ReactNode;
zoomOut?: React.ReactNode;
close?: React.ReactNode;
prev?: React.ReactNode;
next?: React.ReactNode;
/** @deprecated Please use `prev` instead */
left?: React.ReactNode;
/** @deprecated Please use `next` instead */
right?: React.ReactNode;
flipX?: React.ReactNode;
flipY?: React.ReactNode;
}
export interface Actions {
onActive: (offset: number) => void;
onFlipY: () => void;
onFlipX: () => void;
onRotateLeft: () => void;
onRotateRight: () => void;
onZoomOut: () => void;
onZoomIn: () => void;
onClose: () => void;
onReset: () => void;
}
export type ToolbarRenderInfoType = {
icons: {
prevIcon?: React.ReactNode;
nextIcon?: React.ReactNode;
flipYIcon: React.ReactNode;
flipXIcon: React.ReactNode;
rotateLeftIcon: React.ReactNode;
rotateRightIcon: React.ReactNode;
zoomOutIcon: React.ReactNode;
zoomInIcon: React.ReactNode;
};
actions: Actions;
transform: TransformType;
current: number;
total: number;
image: ImgInfo;
};
export interface InternalPreviewConfig {
/** Better to use `classNames.root` instead */
rootClassName?: string;
src?: string;
alt?: string;
scaleStep?: number;
minScale?: number;
maxScale?: number;
motionName?: string;
open?: boolean;
getContainer?: PortalProps['getContainer'];
zIndex?: number;
maskClosable?: boolean;
afterOpenChange?: (open: boolean) => void;
/** Whether to trap focus within the preview when open. Default is true. */
focusTrap?: boolean;
movable?: boolean;
icons?: OperationIcons;
closeIcon?: React.ReactNode;
onTransform?: (info: {
transform: TransformType;
action: TransformAction;
}) => void;
countRender?: (current: number, total: number) => React.ReactNode;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
current?: number;
image: ImgInfo;
}) => React.ReactNode;
actionsRender?: (originalNode: React.ReactElement, info: ToolbarRenderInfoType) => React.ReactNode;
}
export interface PreviewProps extends InternalPreviewConfig {
prefixCls: string;
classNames?: Partial<Record<PreviewSemanticName, string>>;
styles?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
imageInfo?: {
width: number | string;
height: number | string;
};
fallback?: string;
imgCommonProps?: React.ImgHTMLAttributes<HTMLImageElement>;
width?: string | number;
height?: string | number;
current?: number;
count?: number;
onChange?: (current: number, prev: number) => void;
onClose?: () => void;
mousePosition: null | {
x: number;
y: number;
};
}
declare const Preview: React.FC<PreviewProps>;
export default Preview;
+371
View File
@@ -0,0 +1,371 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _motion = _interopRequireDefault(require("@rc-component/motion"));
var _portal = _interopRequireDefault(require("@rc-component/portal"));
var _util = require("@rc-component/util");
var _focus = require("@rc-component/util/lib/Dom/focus");
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _KeyCode = _interopRequireDefault(require("@rc-component/util/lib/KeyCode"));
var _clsx = require("clsx");
var _react = _interopRequireWildcard(require("react"));
var _context = require("../context");
var _useImageTransform = _interopRequireDefault(require("../hooks/useImageTransform"));
var _useMouseEvent = _interopRequireDefault(require("../hooks/useMouseEvent"));
var _useStatus = _interopRequireDefault(require("../hooks/useStatus"));
var _useTouchEvent = _interopRequireDefault(require("../hooks/useTouchEvent"));
var _previewConfig = require("../previewConfig");
var _CloseBtn = _interopRequireDefault(require("./CloseBtn"));
var _Footer = _interopRequireDefault(require("./Footer"));
var _PrevNext = _interopRequireDefault(require("./PrevNext"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
// Note: if you want to add `action`,
// pls contact @zombieJ or @thinkasany first.
const PreviewImage = ({
fallback,
src,
imgRef,
...props
}) => {
const [getImgRef, srcAndOnload] = (0, _useStatus.default)({
src,
fallback
});
return /*#__PURE__*/_react.default.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 = (0, _react.useRef)();
const wrapperRef = (0, _react.useRef)(null);
const triggerRef = (0, _react.useRef)(null);
const groupContext = (0, _react.useContext)(_context.PreviewGroupContext);
const showLeftOrRightSwitches = groupContext && count > 1;
const showOperationsProgress = groupContext && count >= 1;
// ======================== Transform =========================
const [enableTransition, setEnableTransition] = (0, _react.useState)(true);
const {
transform,
resetTransform,
updateTransform,
dispatchZoomChange
} = (0, _useImageTransform.default)(imgRef, minScale, maxScale, onTransform);
const {
isMoving,
onMouseDown,
onWheel
} = (0, _useMouseEvent.default)(imgRef, movable, open, scaleStep, transform, updateTransform, dispatchZoomChange);
const {
isTouching,
onTouchStart,
onTouchMove,
onTouchEnd
} = (0, _useTouchEvent.default)(imgRef, movable, open, minScale, transform, updateTransform, dispatchZoomChange);
const {
rotate,
scale
} = transform;
(0, _react.useEffect)(() => {
if (!enableTransition) {
setEnableTransition(true);
}
}, [enableTransition]);
(0, _react.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(_previewConfig.BASE_SCALE_RATIO + scaleStep, 'doubleClick', event.clientX, event.clientY);
}
}
};
const imgNode = /*#__PURE__*/_react.default.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(_previewConfig.BASE_SCALE_RATIO + scaleStep, 'zoomIn');
};
const onZoomOut = () => {
dispatchZoomChange(_previewConfig.BASE_SCALE_RATIO / (_previewConfig.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 = (0, _util.useEvent)(event => {
if (open) {
const {
keyCode
} = event;
if (showLeftOrRightSwitches) {
if (keyCode === _KeyCode.default.LEFT) {
onActive(-1);
} else if (keyCode === _KeyCode.default.RIGHT) {
onActive(1);
}
}
}
});
(0, _react.useEffect)(() => {
if (open) {
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('keydown', onKeyDown);
};
}
}, [open]);
// ======================= Lock Scroll ========================
const [lockScroll, setLockScroll] = (0, _react.useState)(false);
_react.default.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] = (0, _react.useState)(false);
(0, _useLayoutEffect.default)(() => {
if (open) {
setPortalRender(true);
}
}, [open]);
const onEsc = ({
top
}) => {
if (top) {
onClose?.();
}
};
// =========================== Focus ============================
(0, _useLayoutEffect.default)(() => {
if (open) {
triggerRef.current = document.activeElement;
}
}, [open]);
(0, _focus.useLockFocus)(focusTrap && open && portalRender, () => wrapperRef.current);
// ========================== Render ==========================
const bodyStyle = {
...styles.body
};
if (mousePosition) {
bodyStyle.transformOrigin = `${mousePosition.x}px ${mousePosition.y}px`;
}
return /*#__PURE__*/_react.default.createElement(_portal.default, {
open: portalRender && open,
autoDestroy: false,
getContainer: getContainer,
autoLock: lockScroll,
onEsc: onEsc
}, /*#__PURE__*/_react.default.createElement(_motion.default, {
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.default.createElement("div", {
ref: wrapperRef,
className: (0, _clsx.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.default.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-mask`, classNames.mask),
style: styles.mask,
onClick: maskClosable ? onClose : undefined
}), /*#__PURE__*/_react.default.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-body`, classNames.body),
style: bodyStyle
}, imageRender ? imageRender(imgNode, {
transform,
image,
...(groupContext ? {
current
} : {})
}) : imgNode), closeIcon !== false && closeIcon !== null && /*#__PURE__*/_react.default.createElement(_CloseBtn.default, {
prefixCls: prefixCls,
icon: closeIcon === true ? icons.close : closeIcon || icons.close,
onClick: onClose,
className: classNames.close,
style: styles.close
}), showLeftOrRightSwitches && /*#__PURE__*/_react.default.createElement(_PrevNext.default, {
prefixCls: prefixCls,
current: current,
count: count,
icons: icons,
onActive: onActive
}), /*#__PURE__*/_react.default.createElement(_Footer.default, {
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
}));
}));
};
var _default = exports.default = Preview;
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react';
import type { ImgInfo } from './Image';
import type { InternalPreviewConfig, PreviewProps, PreviewSemanticName } from './Preview';
import type { TransformType } from './hooks/useImageTransform';
import type { ImageElementProps } from './interface';
export interface GroupPreviewConfig extends InternalPreviewConfig {
current?: number;
imageRender?: (originalNode: React.ReactElement, info: {
transform: TransformType;
current: number;
image: ImgInfo;
}) => React.ReactNode;
onOpenChange?: (value: boolean, info: {
current: number;
}) => void;
onChange?: (current: number, prevCurrent: number) => void;
}
export interface PreviewGroupProps {
previewPrefixCls?: string;
classNames?: {
popup?: Partial<Record<PreviewSemanticName, string>>;
};
styles?: {
popup?: Partial<Record<PreviewSemanticName, React.CSSProperties>>;
};
icons?: PreviewProps['icons'];
items?: (string | ImageElementProps)[];
fallback?: string;
preview?: boolean | GroupPreviewConfig;
children?: React.ReactNode;
}
declare const Group: React.FC<PreviewGroupProps>;
export default Group;
+121
View File
@@ -0,0 +1,121 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
var _useEvent = _interopRequireDefault(require("@rc-component/util/lib/hooks/useEvent"));
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _Preview = _interopRequireDefault(require("./Preview"));
var _context = require("./context");
var _usePreviewItems = _interopRequireDefault(require("./hooks/usePreviewItems"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
const Group = ({
previewPrefixCls = 'rc-image-preview',
classNames,
styles,
children,
icons = {},
items,
preview,
fallback
}) => {
const {
open: previewOpen,
onOpenChange,
current: currentIndex,
onChange,
...restProps
} = preview && typeof preview === 'object' ? preview : {};
// ========================== Items ===========================
const [mergedItems, register, fromItems] = (0, _usePreviewItems.default)(items);
// ========================= Preview ==========================
// >>> Index
const [current, setCurrent] = (0, _useControlledState.default)(0, currentIndex);
const [keepOpenIndex, setKeepOpenIndex] = (0, _react.useState)(false);
// >>> Image
const {
src,
...imgCommonProps
} = mergedItems[current]?.data || {};
// >>> Visible
const [isShowPreview, setShowPreview] = (0, _useControlledState.default)(!!previewOpen, previewOpen);
const triggerShowPreview = (0, _useEvent.default)(next => {
setShowPreview(next);
if (next !== isShowPreview) {
onOpenChange?.(next, {
current
});
}
});
// >>> Position
const [mousePosition, setMousePosition] = (0, _react.useState)(null);
const onPreviewFromImage = React.useCallback((id, imageSrc, mouseX, mouseY) => {
const index = fromItems ? mergedItems.findIndex(item => item.data.src === imageSrc) : mergedItems.findIndex(item => item.id === id);
setCurrent(index < 0 ? 0 : index);
triggerShowPreview(true);
setMousePosition({
x: mouseX,
y: mouseY
});
setKeepOpenIndex(true);
}, [mergedItems, fromItems]);
// Reset current when reopen
React.useEffect(() => {
if (isShowPreview) {
if (!keepOpenIndex) {
setCurrent(0);
}
} else {
setKeepOpenIndex(false);
}
}, [isShowPreview]);
// ========================== Events ==========================
const onInternalChange = (next, prev) => {
setCurrent(next);
onChange?.(next, prev);
};
const onPreviewClose = () => {
triggerShowPreview(false);
setMousePosition(null);
};
// ========================= Context ==========================
const previewGroupContext = React.useMemo(() => ({
register,
onPreview: onPreviewFromImage
}), [register, onPreviewFromImage]);
// ========================== Render ==========================
return /*#__PURE__*/React.createElement(_context.PreviewGroupContext.Provider, {
value: previewGroupContext
}, children, /*#__PURE__*/React.createElement(_Preview.default, _extends({
"aria-hidden": !isShowPreview,
open: isShowPreview,
prefixCls: previewPrefixCls,
onClose: onPreviewClose,
mousePosition: mousePosition,
imgCommonProps: imgCommonProps,
src: src,
fallback: fallback,
icons: icons,
current: current,
count: mergedItems.length,
onChange: onInternalChange
}, restProps, {
classNames: classNames?.popup,
styles: styles?.popup
})));
};
var _default = exports.default = Group;
+2
View File
@@ -0,0 +1,2 @@
import type { ImageElementProps } from './interface';
export declare const COMMON_PROPS: (keyof Omit<ImageElementProps, 'src'>)[];
+7
View File
@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.COMMON_PROPS = void 0;
const COMMON_PROPS = exports.COMMON_PROPS = ['crossOrigin', 'decoding', 'draggable', 'loading', 'referrerPolicy', 'sizes', 'srcSet', 'useMap', 'alt', 'fetchPriority'];
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react';
import type { OnGroupPreview, RegisterImage } from './interface';
export interface PreviewGroupContextProps {
register: RegisterImage;
onPreview: OnGroupPreview;
}
export declare const PreviewGroupContext: React.Context<PreviewGroupContextProps>;
+10
View File
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PreviewGroupContext = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const PreviewGroupContext = exports.PreviewGroupContext = /*#__PURE__*/React.createContext(null);
@@ -0,0 +1,16 @@
/**
* Fix positon x,y point when
*
* Ele width && height < client
* - Back origin
*
* - Ele width | height > clientWidth | clientHeight
* - left | top > 0 -> Back 0
* - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight
*
* Regardless of other
*/
export default function getFixScaleEleTransPosition(width: number, height: number, left: number, top: number): null | {
x: number;
y: number;
};
@@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getFixScaleEleTransPosition;
var _util = require("./util");
function fixPoint(key, start, width, clientWidth) {
const startAddWidth = start + width;
const offsetStart = (width - clientWidth) / 2;
if (width > clientWidth) {
if (start > 0) {
return {
[key]: offsetStart
};
}
if (start < 0 && startAddWidth < clientWidth) {
return {
[key]: -offsetStart
};
}
} else if (start < 0 || startAddWidth > clientWidth) {
return {
[key]: start < 0 ? offsetStart : -offsetStart
};
}
return {};
}
/**
* Fix positon x,y point when
*
* Ele width && height < client
* - Back origin
*
* - Ele width | height > clientWidth | clientHeight
* - left | top > 0 -> Back 0
* - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight
*
* Regardless of other
*/
function getFixScaleEleTransPosition(width, height, left, top) {
const {
width: clientWidth,
height: clientHeight
} = (0, _util.getClientSize)();
let fixPos = null;
if (width <= clientWidth && height <= clientHeight) {
fixPos = {
x: 0,
y: 0
};
} else if (width > clientWidth || height > clientHeight) {
fixPos = {
...fixPoint('x', left, width, clientWidth),
...fixPoint('y', top, height, clientHeight)
};
}
return fixPos;
}
@@ -0,0 +1,28 @@
/// <reference types="react" />
export type TransformType = {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
};
export type TransformAction = 'flipY' | 'flipX' | 'rotateLeft' | 'rotateRight' | 'zoomIn' | 'zoomOut' | 'close' | 'prev' | 'next' | 'wheel' | 'doubleClick' | 'move' | 'dragRebound' | 'touchZoom' | 'reset';
export type UpdateTransformFunc = (newTransform: Partial<TransformType>, action: TransformAction) => void;
export type DispatchZoomChangeFunc = (ratio: number, action: TransformAction, centerX?: number, centerY?: number, isTouch?: boolean) => void;
export default function useImageTransform(imgRef: React.MutableRefObject<HTMLImageElement>, minScale: number, maxScale: number, onTransform: (info: {
transform: TransformType;
action: TransformAction;
}) => void): {
transform: {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
};
resetTransform: (action: TransformAction) => void;
updateTransform: UpdateTransformFunc;
dispatchZoomChange: DispatchZoomChangeFunc;
};
@@ -0,0 +1,125 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useImageTransform;
var _util = require("../util");
var _isEqual = _interopRequireDefault(require("@rc-component/util/lib/isEqual"));
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var _react = require("react");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const initialTransform = {
x: 0,
y: 0,
rotate: 0,
scale: 1,
flipX: false,
flipY: false
};
function useImageTransform(imgRef, minScale, maxScale, onTransform) {
const frame = (0, _react.useRef)(null);
const queue = (0, _react.useRef)([]);
const [transform, setTransform] = (0, _react.useState)(initialTransform);
const resetTransform = action => {
setTransform(initialTransform);
if (!(0, _isEqual.default)(initialTransform, transform)) {
onTransform?.({
transform: initialTransform,
action
});
}
};
/** Direct update transform */
const updateTransform = (newTransform, action) => {
if (frame.current === null) {
queue.current = [];
frame.current = (0, _raf.default)(() => {
setTransform(preState => {
let memoState = preState;
queue.current.forEach(queueState => {
memoState = {
...memoState,
...queueState
};
});
frame.current = null;
onTransform?.({
transform: memoState,
action
});
return memoState;
});
});
}
queue.current.push({
...transform,
...newTransform
});
};
/** Scale according to the position of centerX and centerY */
const dispatchZoomChange = (ratio, action, centerX, centerY, isTouch) => {
const {
width,
height,
offsetWidth,
offsetHeight,
offsetLeft,
offsetTop
} = imgRef.current;
let newRatio = ratio;
let newScale = transform.scale * ratio;
if (newScale > maxScale) {
newScale = maxScale;
newRatio = maxScale / transform.scale;
} else if (newScale < minScale) {
// For mobile interactions, allow scaling down to the minimum scale.
newScale = isTouch ? newScale : minScale;
newRatio = newScale / transform.scale;
}
/** Default center point scaling */
const mergedCenterX = centerX ?? innerWidth / 2;
const mergedCenterY = centerY ?? innerHeight / 2;
const diffRatio = newRatio - 1;
/** Deviation calculated from image size */
const diffImgX = diffRatio * width * 0.5;
const diffImgY = diffRatio * height * 0.5;
/** The difference between the click position and the edge of the document */
const diffOffsetLeft = diffRatio * (mergedCenterX - transform.x - offsetLeft);
const diffOffsetTop = diffRatio * (mergedCenterY - transform.y - offsetTop);
/** Final positioning */
let newX = transform.x - (diffOffsetLeft - diffImgX);
let newY = transform.y - (diffOffsetTop - diffImgY);
/**
* When zooming the image
* When the image size is smaller than the width and height of the window, the position is initialized
*/
if (ratio < 1 && newScale === 1) {
const mergedWidth = offsetWidth * newScale;
const mergedHeight = offsetHeight * newScale;
const {
width: clientWidth,
height: clientHeight
} = (0, _util.getClientSize)();
if (mergedWidth <= clientWidth && mergedHeight <= clientHeight) {
newX = 0;
newY = 0;
}
}
updateTransform({
x: newX,
y: newY,
scale: newScale
}, action);
};
return {
transform,
resetTransform,
updateTransform,
dispatchZoomChange
};
}
@@ -0,0 +1,9 @@
import type React from 'react';
import type { DispatchZoomChangeFunc, TransformType, UpdateTransformFunc } from './useImageTransform';
export default function useMouseEvent(imgRef: React.MutableRefObject<HTMLImageElement>, movable: boolean, open: boolean, scaleStep: number, transform: TransformType, updateTransform: UpdateTransformFunc, dispatchZoomChange: DispatchZoomChangeFunc): {
isMoving: boolean;
onMouseDown: React.MouseEventHandler<HTMLDivElement>;
onMouseMove: (event: MouseEvent) => void;
onMouseUp: () => void;
onWheel: (event: React.WheelEvent<HTMLImageElement>) => void;
};
@@ -0,0 +1,123 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useMouseEvent;
var _warning = require("@rc-component/util/lib/warning");
var _react = require("react");
var _getFixScaleEleTransPosition = _interopRequireDefault(require("../getFixScaleEleTransPosition"));
var _previewConfig = require("../previewConfig");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function useMouseEvent(imgRef, movable, open, scaleStep, transform, updateTransform, dispatchZoomChange) {
const {
rotate,
scale,
x,
y
} = transform;
const [isMoving, setMoving] = (0, _react.useState)(false);
const startPositionInfo = (0, _react.useRef)({
diffX: 0,
diffY: 0,
transformX: 0,
transformY: 0
});
const onMouseDown = event => {
// Only allow main button
if (!movable || event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
startPositionInfo.current = {
diffX: event.pageX - x,
diffY: event.pageY - y,
transformX: x,
transformY: y
};
setMoving(true);
};
const onMouseMove = event => {
if (open && isMoving) {
updateTransform({
x: event.pageX - startPositionInfo.current.diffX,
y: event.pageY - startPositionInfo.current.diffY
}, 'move');
}
};
const onMouseUp = () => {
if (open && isMoving) {
setMoving(false);
/** No need to restore the position when the picture is not moved, So as not to interfere with the click */
const {
transformX,
transformY
} = startPositionInfo.current;
const hasChangedPosition = x !== transformX && y !== transformY;
if (!hasChangedPosition) return;
const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
const {
left,
top
} = imgRef.current.getBoundingClientRect();
const isRotate = rotate % 180 !== 0;
const fixState = (0, _getFixScaleEleTransPosition.default)(isRotate ? height : width, isRotate ? width : height, left, top);
if (fixState) {
updateTransform({
...fixState
}, 'dragRebound');
}
}
};
const onWheel = event => {
if (!open || event.deltaY == 0) return;
// Scale ratio depends on the deltaY size
const scaleRatio = Math.abs(event.deltaY / 100);
// Limit the maximum scale ratio
const mergedScaleRatio = Math.min(scaleRatio, _previewConfig.WHEEL_MAX_SCALE_RATIO);
// Scale the ratio each time
let ratio = _previewConfig.BASE_SCALE_RATIO + mergedScaleRatio * scaleStep;
if (event.deltaY > 0) {
ratio = _previewConfig.BASE_SCALE_RATIO / ratio;
}
dispatchZoomChange(ratio, 'wheel', event.clientX, event.clientY);
};
(0, _react.useEffect)(() => {
if (movable) {
window.addEventListener('mouseup', onMouseUp, false);
window.addEventListener('mousemove', onMouseMove, false);
try {
// Resolve if in iframe lost event
/* istanbul ignore next */
if (window.top !== window.self) {
window.top.addEventListener('mouseup', onMouseUp, false);
window.top.addEventListener('mousemove', onMouseMove, false);
}
} catch (error) {
/* istanbul ignore next */
(0, _warning.warning)(false, `[rc-image] ${error}`);
}
}
return () => {
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('mousemove', onMouseMove);
/* istanbul ignore next */
try {
window.top?.removeEventListener('mouseup', onMouseUp);
window.top?.removeEventListener('mousemove', onMouseMove);
} catch (error) {
// Do nothing
}
};
}, [open, isMoving, x, y, rotate, movable]);
return {
isMoving,
onMouseDown,
onMouseMove,
onMouseUp,
onWheel
};
}
@@ -0,0 +1,7 @@
import type { PreviewGroupProps } from '../PreviewGroup';
import type { InternalItem, RegisterImage } from '../interface';
export type Items = Omit<InternalItem, 'canPreview'>[];
/**
* Merge props provided `items` or context collected images
*/
export default function usePreviewItems(items?: PreviewGroupProps['items']): [items: Items, registerImage: RegisterImage, fromItems: boolean];
@@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = usePreviewItems;
var React = _interopRequireWildcard(require("react"));
var _common = require("../common");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
* Merge props provided `items` or context collected images
*/
function usePreviewItems(items) {
// Context collection image data
const [images, setImages] = React.useState({});
const registerImage = React.useCallback((id, data) => {
setImages(imgs => ({
...imgs,
[id]: data
}));
return () => {
setImages(imgs => {
const cloneImgs = {
...imgs
};
delete cloneImgs[id];
return cloneImgs;
});
};
}, []);
// items
const mergedItems = React.useMemo(() => {
// use `items` first
if (items) {
return items.map(item => {
if (typeof item === 'string') {
return {
data: {
src: item
}
};
}
const data = {};
Object.keys(item).forEach(key => {
if (['src', ..._common.COMMON_PROPS].includes(key)) {
data[key] = item[key];
}
});
return {
data
};
});
}
// use registered images secondly
return Object.keys(images).reduce((total, id) => {
const {
canPreview,
data
} = images[id];
if (canPreview) {
total.push({
data,
id
});
}
return total;
}, []);
}, [items, images]);
return [mergedItems, registerImage, !!items];
}
@@ -0,0 +1,2 @@
import type { ImageElementProps } from '../interface';
export default function useRegisterImage(canPreview: boolean, data: ImageElementProps): string;
@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useRegisterImage;
var React = _interopRequireWildcard(require("react"));
var _context = require("../context");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
let uid = 0;
function useRegisterImage(canPreview, data) {
const [id] = React.useState(() => {
uid += 1;
return String(uid);
});
const groupContext = React.useContext(_context.PreviewGroupContext);
const registerData = {
data,
canPreview
};
// Keep order start
// Resolve https://github.com/ant-design/ant-design/issues/28881
// Only need unRegister when component unMount
React.useEffect(() => {
if (groupContext) {
return groupContext.register(id, registerData);
}
}, []);
React.useEffect(() => {
if (groupContext) {
groupContext.register(id, registerData);
}
}, [canPreview, data]);
return id;
}
@@ -0,0 +1,13 @@
type ImageStatus = 'normal' | 'error' | 'loading';
export default function useStatus({ src, isCustomPlaceholder, fallback, }: {
src: string;
isCustomPlaceholder?: boolean;
fallback?: string;
}): readonly [(img?: HTMLImageElement) => void, {
src: string;
onLoad?: undefined;
} | {
onLoad: () => void;
src: string;
}, ImageStatus];
export {};
@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useStatus;
var _react = require("react");
var _util = require("../util");
function useStatus({
src,
isCustomPlaceholder,
fallback
}) {
const [status, setStatus] = (0, _react.useState)(isCustomPlaceholder ? 'loading' : 'normal');
const isLoaded = (0, _react.useRef)(false);
const isError = status === 'error';
// https://github.com/react-component/image/pull/187
(0, _react.useEffect)(() => {
let isCurrentSrc = true;
(0, _util.isImageValid)(src).then(isValid => {
// https://github.com/ant-design/ant-design/issues/44948
// If src changes, the previous setStatus should not be triggered
if (!isValid && isCurrentSrc) {
setStatus('error');
}
});
return () => {
isCurrentSrc = false;
};
}, [src]);
(0, _react.useEffect)(() => {
if (isCustomPlaceholder && !isLoaded.current) {
setStatus('loading');
} else if (isError) {
setStatus('normal');
}
}, [src]);
const onLoad = () => {
setStatus('normal');
};
const getImgRef = img => {
isLoaded.current = false;
if (status === 'loading' && img?.complete && (img.naturalWidth || img.naturalHeight)) {
isLoaded.current = true;
onLoad();
}
};
const srcAndOnload = isError && fallback ? {
src: fallback
} : {
onLoad,
src
};
return [getImgRef, srcAndOnload, status];
}
@@ -0,0 +1,8 @@
import type React from 'react';
import type { DispatchZoomChangeFunc, TransformType, UpdateTransformFunc } from './useImageTransform';
export default function useTouchEvent(imgRef: React.MutableRefObject<HTMLImageElement>, movable: boolean, open: boolean, minScale: number, transform: TransformType, updateTransform: UpdateTransformFunc, dispatchZoomChange: DispatchZoomChangeFunc): {
isTouching: boolean;
onTouchStart: (event: React.TouchEvent<HTMLImageElement>) => void;
onTouchMove: (event: React.TouchEvent<HTMLImageElement>) => void;
onTouchEnd: () => void;
};
@@ -0,0 +1,177 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useTouchEvent;
var _react = require("react");
var _getFixScaleEleTransPosition = _interopRequireDefault(require("../getFixScaleEleTransPosition"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function getDistance(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.hypot(x, y);
}
function getCenter(oldPoint1, oldPoint2, newPoint1, newPoint2) {
// Calculate the distance each point has moved
const distance1 = getDistance(oldPoint1, newPoint1);
const distance2 = getDistance(oldPoint2, newPoint2);
// If both distances are 0, return the original points
if (distance1 === 0 && distance2 === 0) {
return [oldPoint1.x, oldPoint1.y];
}
// Calculate the ratio of the distances
const ratio = distance1 / (distance1 + distance2);
// Calculate the new center point based on the ratio
const x = oldPoint1.x + ratio * (oldPoint2.x - oldPoint1.x);
const y = oldPoint1.y + ratio * (oldPoint2.y - oldPoint1.y);
return [x, y];
}
function useTouchEvent(imgRef, movable, open, minScale, transform, updateTransform, dispatchZoomChange) {
const {
rotate,
scale,
x,
y
} = transform;
const [isTouching, setIsTouching] = (0, _react.useState)(false);
const touchPointInfo = (0, _react.useRef)({
point1: {
x: 0,
y: 0
},
point2: {
x: 0,
y: 0
},
eventType: 'none'
});
const updateTouchPointInfo = values => {
touchPointInfo.current = {
...touchPointInfo.current,
...values
};
};
const onTouchStart = event => {
if (!movable) return;
event.stopPropagation();
setIsTouching(true);
const {
touches = []
} = event;
if (touches.length > 1) {
// touch zoom
updateTouchPointInfo({
point1: {
x: touches[0].clientX,
y: touches[0].clientY
},
point2: {
x: touches[1].clientX,
y: touches[1].clientY
},
eventType: 'touchZoom'
});
} else {
// touch move
updateTouchPointInfo({
point1: {
x: touches[0].clientX - x,
y: touches[0].clientY - y
},
eventType: 'move'
});
}
};
const onTouchMove = event => {
const {
touches = []
} = event;
const {
point1,
point2,
eventType
} = touchPointInfo.current;
if (touches.length > 1 && eventType === 'touchZoom') {
// touch zoom
const newPoint1 = {
x: touches[0].clientX,
y: touches[0].clientY
};
const newPoint2 = {
x: touches[1].clientX,
y: touches[1].clientY
};
const [centerX, centerY] = getCenter(point1, point2, newPoint1, newPoint2);
const ratio = getDistance(newPoint1, newPoint2) / getDistance(point1, point2);
dispatchZoomChange(ratio, 'touchZoom', centerX, centerY, true);
updateTouchPointInfo({
point1: newPoint1,
point2: newPoint2,
eventType: 'touchZoom'
});
} else if (eventType === 'move') {
// touch move
updateTransform({
x: touches[0].clientX - point1.x,
y: touches[0].clientY - point1.y
}, 'move');
updateTouchPointInfo({
eventType: 'move'
});
}
};
const onTouchEnd = () => {
if (!open) return;
if (isTouching) {
setIsTouching(false);
}
updateTouchPointInfo({
eventType: 'none'
});
if (minScale > scale) {
/** When the scaling ratio is less than the minimum scaling ratio, reset the scaling ratio */
return updateTransform({
x: 0,
y: 0,
scale: minScale
}, 'touchZoom');
}
const width = imgRef.current.offsetWidth * scale;
const height = imgRef.current.offsetHeight * scale;
// eslint-disable-next-line @typescript-eslint/no-shadow
const {
left,
top
} = imgRef.current.getBoundingClientRect();
const isRotate = rotate % 180 !== 0;
const fixState = (0, _getFixScaleEleTransPosition.default)(isRotate ? height : width, isRotate ? width : height, left, top);
if (fixState) {
updateTransform({
...fixState
}, 'dragRebound');
}
};
(0, _react.useEffect)(() => {
const preventDefault = e => {
e.preventDefault();
};
if (open && movable) {
window.addEventListener('touchmove', preventDefault, {
passive: false
});
}
return () => {
window.removeEventListener('touchmove', preventDefault);
};
}, [open, movable]);
return {
isTouching,
onTouchStart,
onTouchMove,
onTouchEnd
};
}
+3
View File
@@ -0,0 +1,3 @@
import Image from './Image';
export * from './Image';
export default Image;
+22
View File
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
exports.default = void 0;
var _Image = _interopRequireWildcard(require("./Image"));
Object.keys(_Image).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _Image[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _Image[key];
}
});
});
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
var _default = exports.default = _Image.default;
+14
View File
@@ -0,0 +1,14 @@
/// <reference types="react" />
/**
* Used for PreviewGroup passed image data
*/
export type ImageElementProps = Pick<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'crossOrigin' | 'decoding' | 'draggable' | 'loading' | 'referrerPolicy' | 'sizes' | 'srcSet' | 'useMap' | 'alt' | 'fetchPriority'>;
export type PreviewImageElementProps = {
data: ImageElementProps;
canPreview: boolean;
};
export type InternalItem = PreviewImageElementProps & {
id?: string;
};
export type RegisterImage = (id: string, data: PreviewImageElementProps) => VoidFunction;
export type OnGroupPreview = (id: string, imageSrc: string, mouseX: number, mouseY: number) => void;
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
@@ -0,0 +1,4 @@
/** Scale the ratio base */
export declare const BASE_SCALE_RATIO = 1;
/** The maximum zoom ratio when the mouse zooms in, adjustable */
export declare const WHEEL_MAX_SCALE_RATIO = 1;
+10
View File
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.WHEEL_MAX_SCALE_RATIO = exports.BASE_SCALE_RATIO = void 0;
/** Scale the ratio base */
const BASE_SCALE_RATIO = exports.BASE_SCALE_RATIO = 1;
/** The maximum zoom ratio when the mouse zooms in, adjustable */
const WHEEL_MAX_SCALE_RATIO = exports.WHEEL_MAX_SCALE_RATIO = 1;
+5
View File
@@ -0,0 +1,5 @@
export declare function isImageValid(src: string): Promise<unknown>;
export declare function getClientSize(): {
width: number;
height: number;
};
+29
View File
@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getClientSize = getClientSize;
exports.isImageValid = isImageValid;
function isImageValid(src) {
return new Promise(resolve => {
if (!src) {
resolve(false);
return;
}
const img = document.createElement('img');
img.onerror = () => resolve(false);
img.onload = () => resolve(true);
img.src = src;
});
}
// ============================= Legacy =============================
function getClientSize() {
const width = document.documentElement.clientWidth;
const height = window.innerHeight || document.documentElement.clientHeight;
return {
width,
height
};
}
+74
View File
@@ -0,0 +1,74 @@
{
"name": "@rc-component/image",
"version": "1.9.0",
"description": "React easy to use image component",
"keywords": [
"react",
"react-component",
"react-image",
"image"
],
"homepage": "http://github.com/react-component/image",
"bugs": {
"url": "http://github.com/react-component/image/issues"
},
"repository": {
"type": "git",
"url": "git@github.com:react-component/image.git"
},
"license": "MIT",
"main": "./lib/index",
"module": "./es/index",
"types": "./lib/index.d.ts",
"files": [
"assets/*.css",
"es",
"lib"
],
"scripts": {
"compile": "father build && lessc assets/index.less assets/index.css",
"coverage": "rc-test --coverage",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
"now-build": "npm run docs:build",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"start": "dumi dev",
"test": "rc-test",
"test:update": "rc-test -u",
"tsc": "bunx tsc --noEmit"
},
"dependencies": {
"@rc-component/motion": "^1.0.0",
"@rc-component/portal": "^2.1.2",
"@rc-component/util": "^1.10.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@ant-design/icons": "^5.0.1",
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/np": "^1.0.0",
"@rc-component/dialog": "^1.7.0",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^15.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/fabric": "^4.0.1",
"dumi": "^2.1.4",
"eslint": "^8.57.0",
"father": "^4.0.0",
"glob": "^11.0.3",
"less": "^4.1.3",
"rc-test": "^7.0.3",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^5.3.3"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
}