From e58eda3bae90be09bc4424870c8e49fe6d8bd284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 12 Mar 2025 15:56:55 +0800 Subject: [PATCH 01/25] chore: rm Dialog --- package.json | 1 - src/Preview.tsx | 106 ++++++++++++++++++++++++++---------------------- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index ce8342ac..75c162b1 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@rc-component/portal": "^2.0.0", "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.0.0", - "@rc-component/dialog": "~1.2.0", "classnames": "^2.2.6" }, "devDependencies": { diff --git a/src/Preview.tsx b/src/Preview.tsx index 284c13c6..6f1fe6a5 100644 --- a/src/Preview.tsx +++ b/src/Preview.tsx @@ -1,5 +1,5 @@ import type { DialogProps as IDialogPropTypes } from '@rc-component/dialog'; -import Dialog from '@rc-component/dialog'; +import Portal from '@rc-component/portal'; import KeyCode from '@rc-component/util/lib/KeyCode'; import classnames from 'classnames'; import React, { useContext, useEffect, useRef, useState } from 'react'; @@ -131,7 +131,6 @@ const Preview: React.FC = props => { scaleStep = 0.5, minScale = 1, maxScale = 50, - transitionName = 'zoom', maskTransitionName = 'fade', imageRender, imgCommonProps, @@ -184,9 +183,11 @@ const Preview: React.FC = props => { } }, [enableTransition]); - const onAfterClose = () => { - resetTransform('close'); - }; + useEffect(() => { + if (!visible) { + resetTransform('close'); + } + }, [visible]); const onZoomIn = () => { dispatchZoomChange(BASE_SCALE_RATIO + scaleStep, 'zoomIn'); @@ -228,15 +229,29 @@ const Preview: React.FC = props => { onChange?.(position, current); }; - const onKeyDown = (event: KeyboardEvent) => { - if (!visible || !showLeftOrRightSwitches) return; + useEffect(() => { + const onGlobalKeyDown = (event: KeyboardEvent) => { + if (!visible) return; - if (event.keyCode === KeyCode.LEFT) { - onActive(-1); - } else if (event.keyCode === KeyCode.RIGHT) { - onActive(1); - } - }; + if (event.keyCode === KeyCode.ESC) { + onClose?.(); + } + + if (showLeftOrRightSwitches) { + if (event.keyCode === KeyCode.LEFT) { + onActive(-1); + } else if (event.keyCode === KeyCode.RIGHT) { + onActive(1); + } + } + }; + + window.addEventListener('keydown', onGlobalKeyDown, false); + + return () => { + window.removeEventListener('keydown', onGlobalKeyDown); + }; + }, [visible, showLeftOrRightSwitches, current]); const onDoubleClick = (event: React.MouseEvent) => { if (visible) { @@ -253,14 +268,6 @@ const Preview: React.FC = props => { } }; - useEffect(() => { - window.addEventListener('keydown', onKeyDown, false); - - return () => { - window.removeEventListener('keydown', onKeyDown); - }; - }, [visible, showLeftOrRightSwitches, current]); - const imgNode = ( = props => { return ( <> - -
- {imageRender - ? imageRender(imgNode, { transform, image, ...(groupContext ? { current } : {}) }) - : imgNode} -
-
+ {visible && ( + +
+
+
+
+
+ {imageRender + ? imageRender(imgNode, { transform, image, ...(groupContext ? { current } : {}) }) + : imgNode} +
+
+
+
+ + )} Date: Wed, 12 Mar 2025 18:02:17 +0800 Subject: [PATCH 02/25] chore: refactor --- assets/index.less | 464 ++++++++++++++++++++++---------------------- assets/preview.less | 35 ++++ src/Preview.tsx | 89 ++++++--- 3 files changed, 330 insertions(+), 258 deletions(-) create mode 100644 assets/preview.less diff --git a/assets/index.less b/assets/index.less index c0999ccf..663bf7cc 100644 --- a/assets/index.less +++ b/assets/index.less @@ -1,3 +1,5 @@ +@import 'preview.less'; + @prefixCls: rc-image; @zindex-preview-mask: 1000; @preview-mask-bg: fade(#000, 100%); @@ -6,31 +8,31 @@ @background-color: #f3f3f3; .reset() { + box-sizing: border-box; margin: 0; padding: 0; - box-sizing: border-box; } .box() { position: absolute; top: 0; - left: 0; right: 0; bottom: 0; + left: 0; } .@{prefixCls} { - display: inline-flex; position: relative; + display: inline-flex; &-img { width: 100%; height: auto; &-placeholder { background-color: @background-color; + 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; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjhweCIgaGVpZ2h0PSIyMnB4IiB2aWV3Qm94PSIwIDAgMjggMjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDU1LjIgKDc4MTgxKSAtIGh0dHBzOi8vc2tldGNoYXBwLmNvbSAtLT4KICAgIDx0aXRsZT5pbWFnZS1maWxs5aSH5Lu9PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9Iuafpeeci+WbvueJh+S8mOWMljQuMCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IuWKoOi9veWbvueJhyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTU3Mi4wMDAwMDAsIC01MDYuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJpbWFnZS1maWxs5aSH5Lu9IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1NzAuMDAwMDAwLCA1MDEuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjMDAwMDAwIiBvcGFjaXR5PSIwIiB4PSIwIiB5PSIwIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjwvcmVjdD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOSw1IEwzLDUgQzIuNDQ2ODc1LDUgMiw1LjQ0Njg3NSAyLDYgTDIsMjYgQzIsMjYuNTUzMTI1IDIuNDQ2ODc1LDI3IDMsMjcgTDI5LDI3IEMyOS41NTMxMjUsMjcgMzAsMjYuNTUzMTI1IDMwLDI2IEwzMCw2IEMzMCw1LjQ0Njg3NSAyOS41NTMxMjUsNSAyOSw1IFogTTEwLjU2MjUsOS41IEMxMS42NjU2MjUsOS41IDEyLjU2MjUsMTAuMzk2ODc1IDEyLjU2MjUsMTEuNSBDMTIuNTYyNSwxMi42MDMxMjUgMTEuNjY1NjI1LDEzLjUgMTAuNTYyNSwxMy41IEM5LjQ1OTM3NSwxMy41IDguNTYyNSwxMi42MDMxMjUgOC41NjI1LDExLjUgQzguNTYyNSwxMC4zOTY4NzUgOS40NTkzNzUsOS41IDEwLjU2MjUsOS41IFogTTI2LjYyMTg3NSwyMy4xNTkzNzUgQzI2LjU3ODEyNSwyMy4xOTY4NzUgMjYuNTE4NzUsMjMuMjE4NzUgMjYuNDU5Mzc1LDIzLjIxODc1IEw1LjUzNzUsMjMuMjE4NzUgQzUuNCwyMy4yMTg3NSA1LjI4NzUsMjMuMTA2MjUgNS4yODc1LDIyLjk2ODc1IEM1LjI4NzUsMjIuOTA5Mzc1IDUuMzA5Mzc1LDIyLjg1MzEyNSA1LjM0Njg3NSwyMi44MDYyNSBMMTAuNjY4NzUsMTYuNDkzNzUgQzEwLjc1NjI1LDE2LjM4NzUgMTAuOTE1NjI1LDE2LjM3NSAxMS4wMjE4NzUsMTYuNDYyNSBDMTEuMDMxMjUsMTYuNDcxODc1IDExLjA0Mzc1LDE2LjQ4MTI1IDExLjA1MzEyNSwxNi40OTM3NSBMMTQuMTU5Mzc1LDIwLjE4MTI1IEwxOS4xLDE0LjMyMTg3NSBDMTkuMTg3NSwxNC4yMTU2MjUgMTkuMzQ2ODc1LDE0LjIwMzEyNSAxOS40NTMxMjUsMTQuMjkwNjI1IEMxOS40NjI1LDE0LjMgMTkuNDc1LDE0LjMwOTM3NSAxOS40ODQzNzUsMTQuMzIxODc1IEwyNi42NTkzNzUsMjIuODA5Mzc1IEMyNi43NDA2MjUsMjIuOTEyNSAyNi43MjgxMjUsMjMuMDcxODc1IDI2LjYyMTg3NSwyMy4xNTkzNzUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRThFOEU4Ij48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==); } } @@ -41,226 +43,226 @@ // >>> Mask &-mask { position: absolute; - left: 0; - right: 0; top: 0; + right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.3); - opacity: 0; - pointer-events: none; + 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; } &:hover &-mask { opacity: 1; } - &-preview { - text-align: center; - height: 100%; - pointer-events: none; - - &-body { - .box; - overflow: hidden; - } - - &.zoom-enter, - &.zoom-appear { - transform: none; - opacity: 0; - animation-duration: 0.3s; - } - - &-mask { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-preview-mask; - height: 100%; - background-color: fade(@preview-mask-bg, 45%); - - &-hidden { - display: none; - } - } - - &-img { - cursor: grab; - transform: scale3d(1, 1, 1); - transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; - user-select: none; - vertical-align: middle; - max-width: 100%; - max-height: 70%; - - &-wrapper { - .box; - display: flex; - justify-content: center; - align-items: center; - - & > * { - pointer-events: auto; - } - } - } - - &-moving { - .@{prefixCls}-preview-img { - cursor: grabbing; - &-wrapper { - transition-duration: 0s; - } - } - } - - &-wrap { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-preview-mask; - overflow: auto; - outline: 0; - -webkit-overflow-scrolling: touch; - } - - &-close { - position: fixed; - top: 32px; - right: 32px; - display: flex; - color: #fff; - background-color: rgba(0, 0, 0, 0.5); - border-radius: 50%; - padding: 15px; - outline: 0; - border: 0; - cursor: pointer; - - &:hover { - opacity: 0.3; - } - } - - &-operations-wrapper { - position: fixed; - z-index: @zindex-preview-mask + 1; - } - - &-footer { - position: fixed; - z-index: @zindex-preview-mask + 1; - bottom: 32px; - left: 0; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - } - - &-progress { - margin-bottom: 20px; - } - - &-operations { - display: flex; - color: @text-color; - background: fade(@preview-mask-bg, 45%); - border-radius: 100px; - padding: 0 20px; - - &-operation { - padding: 10px; - cursor: pointer; - margin-left: 10px; - font-size: 18px; - &-disabled { - pointer-events: none; - color: @text-color-disabled; - } - &:first-of-type { - margin-left: 0; - } - } - } - - &-switch-left { - position: fixed; - left: 10px; - top: 50%; - width: 44px; - height: 44px; - margin-top: -22px; - background: fade(@text-color, 45%); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - z-index: @zindex-preview-mask + 1; - cursor: pointer; - color: @text-color; - &-disabled { - background: fade(@text-color, 30%); - color: @text-color-disabled; - cursor: not-allowed; - > .anticon { - cursor: not-allowed; - } - } - > .anticon { - font-size: 24px; - } - } - - &-switch-right { - position: fixed; - right: 10px; - top: 50%; - width: 44px; - height: 44px; - margin-top: -22px; - background: fade(@text-color, 45%); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - z-index: @zindex-preview-mask + 1; - cursor: pointer; - color: @text-color; - &-disabled { - background: fade(@text-color, 20%); - color: @text-color-disabled; - cursor: not-allowed; - > .anticon { - cursor: not-allowed; - } - } - > .anticon { - font-size: 24px; - } - } - } + // &-preview { + // height: 100%; + // text-align: center; + // pointer-events: none; + + // &-body { + // .box; + // overflow: hidden; + // } + + // // &.zoom-enter, + // // &.zoom-appear { + // // transform: none; + // // opacity: 0; + // // animation-duration: 0.3s; + // // } + + // &-mask { + // position: fixed; + // top: 0; + // right: 0; + // bottom: 0; + // left: 0; + // z-index: @zindex-preview-mask; + // height: 100%; + // background-color: fade(@preview-mask-bg, 45%); + + // &-hidden { + // display: none; + // } + // } + + // &-img { + // max-width: 100%; + // max-height: 70%; + // vertical-align: middle; + // transform: scale3d(1, 1, 1); + // cursor: grab; + // transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; + // user-select: none; + + // &-wrapper { + // .box; + // display: flex; + // align-items: center; + // justify-content: center; + + // & > * { + // pointer-events: auto; + // } + // } + // } + + // &-moving { + // .@{prefixCls}-preview-img { + // cursor: grabbing; + // &-wrapper { + // transition-duration: 0s; + // } + // } + // } + + // &-wrap { + // position: fixed; + // top: 0; + // right: 0; + // bottom: 0; + // left: 0; + // z-index: @zindex-preview-mask; + // overflow: auto; + // outline: 0; + // -webkit-overflow-scrolling: touch; + // } + + // &-close { + // position: fixed; + // top: 32px; + // right: 32px; + // display: flex; + // padding: 15px; + // color: #fff; + // background-color: rgba(0, 0, 0, 0.5); + // border: 0; + // border-radius: 50%; + // outline: 0; + // cursor: pointer; + + // &:hover { + // opacity: 0.3; + // } + // } + + // &-operations-wrapper { + // position: fixed; + // z-index: @zindex-preview-mask + 1; + // } + + // &-footer { + // position: fixed; + // bottom: 32px; + // left: 0; + // z-index: @zindex-preview-mask + 1; + // display: flex; + // flex-direction: column; + // align-items: center; + // width: 100%; + // } + + // &-progress { + // margin-bottom: 20px; + // } + + // &-operations { + // display: flex; + // padding: 0 20px; + // color: @text-color; + // background: fade(@preview-mask-bg, 45%); + // border-radius: 100px; + + // &-operation { + // margin-left: 10px; + // padding: 10px; + // font-size: 18px; + // cursor: pointer; + // &-disabled { + // color: @text-color-disabled; + // pointer-events: none; + // } + // &:first-of-type { + // margin-left: 0; + // } + // } + // } + + // &-switch-left { + // position: fixed; + // top: 50%; + // left: 10px; + // z-index: @zindex-preview-mask + 1; + // display: flex; + // align-items: center; + // justify-content: center; + // width: 44px; + // height: 44px; + // margin-top: -22px; + // color: @text-color; + // background: fade(@text-color, 45%); + // border-radius: 50%; + // cursor: pointer; + // &-disabled { + // color: @text-color-disabled; + // background: fade(@text-color, 30%); + // cursor: not-allowed; + // > .anticon { + // cursor: not-allowed; + // } + // } + // > .anticon { + // font-size: 24px; + // } + // } + + // &-switch-right { + // position: fixed; + // top: 50%; + // right: 10px; + // z-index: @zindex-preview-mask + 1; + // display: flex; + // align-items: center; + // justify-content: center; + // width: 44px; + // height: 44px; + // margin-top: -22px; + // color: @text-color; + // background: fade(@text-color, 45%); + // border-radius: 50%; + // cursor: pointer; + // &-disabled { + // color: @text-color-disabled; + // background: fade(@text-color, 20%); + // cursor: not-allowed; + // > .anticon { + // cursor: not-allowed; + // } + // } + // > .anticon { + // font-size: 24px; + // } + // } + // } } .fade-enter, .fade-appear { animation-duration: 0.3s; - animation-fill-mode: both; animation-play-state: paused; + animation-fill-mode: both; } .fade-leave { animation-duration: 0.3s; - animation-fill-mode: both; animation-play-state: paused; + animation-fill-mode: both; } .fade-enter.fade-enter-active, .fade-appear.fade-appear-active { @@ -299,36 +301,36 @@ } } -.zoom-enter, -.zoom-appear { - animation-duration: 0.3s; - animation-fill-mode: both; - animation-play-state: paused; -} -.zoom-leave { - animation-duration: 0.3s; - animation-fill-mode: both; - animation-play-state: paused; -} -.zoom-enter.zoom-enter-active, -.zoom-appear.zoom-appear-active { - animation-name: rcImageZoomIn; - animation-play-state: running; -} -.zoom-leave.zoom-leave-active { - animation-name: rcImageZoomOut; - animation-play-state: running; - pointer-events: none; -} -.zoom-enter, -.zoom-appear { - transform: scale(0); - opacity: 0; - animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); -} -.zoom-leave { - animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); -} +// .zoom-enter, +// .zoom-appear { +// animation-duration: 0.3s; +// animation-play-state: paused; +// animation-fill-mode: both; +// } +// .zoom-leave { +// animation-duration: 0.3s; +// animation-play-state: paused; +// animation-fill-mode: both; +// } +// .zoom-enter.zoom-enter-active, +// .zoom-appear.zoom-appear-active { +// animation-name: rcImageZoomIn; +// animation-play-state: running; +// } +// .zoom-leave.zoom-leave-active { +// animation-name: rcImageZoomOut; +// animation-play-state: running; +// pointer-events: none; +// } +// .zoom-enter, +// .zoom-appear { +// transform: scale(0); +// opacity: 0; +// animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); +// } +// .zoom-leave { +// animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); +// } @keyframes rcImageZoomIn { 0% { diff --git a/assets/preview.less b/assets/preview.less new file mode 100644 index 00000000..cdb74cbb --- /dev/null +++ b/assets/preview.less @@ -0,0 +1,35 @@ +@import (reference) 'index.less'; + +.zoom { + transition: opacity 0.3s; +} + +.@{prefixCls}-preview { + position: fixed; + overflow: hidden; + inset: 0; + + &-mask { + position: absolute; + background-color: rgba(0, 0, 0, 0.3); + inset: 0; + } + + &-body { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + inset: 0; + + > * { + pointer-events: auto; + } + } + + &-img { + max-width: 100%; + max-height: 70%; + } +} diff --git a/src/Preview.tsx b/src/Preview.tsx index 6f1fe6a5..dfb42d23 100644 --- a/src/Preview.tsx +++ b/src/Preview.tsx @@ -1,10 +1,10 @@ import type { DialogProps as IDialogPropTypes } from '@rc-component/dialog'; +import CSSMotion from '@rc-component/motion'; import Portal from '@rc-component/portal'; import KeyCode from '@rc-component/util/lib/KeyCode'; import classnames from 'classnames'; import React, { useContext, useEffect, useRef, useState } from 'react'; import type { ImgInfo, SemanticName } from './Image'; -import Operations from './Operations'; import { PreviewGroupContext } from './context'; import type { TransformAction, TransformType } from './hooks/useImageTransform'; import useImageTransform from './hooks/useImageTransform'; @@ -131,6 +131,7 @@ const Preview: React.FC = props => { scaleStep = 0.5, minScale = 1, maxScale = 50, + transitionName = 'zoom', maskTransitionName = 'fade', imageRender, imgCommonProps, @@ -304,34 +305,68 @@ const Preview: React.FC = props => { <> {visible && ( -
-
-
-
-
- {imageRender - ? imageRender(imgNode, { transform, image, ...(groupContext ? { current } : {}) }) - : imgNode} -
-
-
+
+ + {({ className: maskMotionClassName, style: maskMotionStyle }) => { + return ( +
+ ); + }} + + + {({ className: bodyMotionClassName, style: bodyMotionStyle }) => { + return ( +
+ {imageRender + ? imageRender(imgNode, { + transform, + image, + ...(groupContext ? { current } : {}), + }) + : imgNode} +
+ ); + + // return ( + //
+ //
+ //
+ //
+ // {imageRender + // ? imageRender(imgNode, { + // transform, + // image, + // ...(groupContext ? { current } : {}), + // }) + // : imgNode} + //
+ //
+ //
+ //
+ // ); + }} +
)} - = props => { image={image} classNames={imageClassNames} styles={styles} - /> + /> */} ); }; From 9c2cd816e87fb00ba99d24c4d2bd13b3d54e1b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 12 Mar 2025 19:29:15 +0800 Subject: [PATCH 03/25] chore: tmp of it --- assets/preview.less | 47 +++++++++ src/Preview/Footer.tsx | 136 +++++++++++++++++++++++++ src/Preview/PrevNext.tsx | 44 ++++++++ src/{Preview.tsx => Preview/index.tsx} | 98 ++++++++++++------ 4 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 src/Preview/Footer.tsx create mode 100644 src/Preview/PrevNext.tsx rename src/{Preview.tsx => Preview/index.tsx} (83%) diff --git a/assets/preview.less b/assets/preview.less index cdb74cbb..907d62b9 100644 --- a/assets/preview.less +++ b/assets/preview.less @@ -7,6 +7,7 @@ .@{prefixCls}-preview { position: fixed; overflow: hidden; + user-select: none; inset: 0; &-mask { @@ -32,4 +33,50 @@ max-width: 100%; max-height: 70%; } + + // =================== Switch =================== + &-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-radius: 9999px; + transform: translateY(-50%); + cursor: pointer; + + &-disabled { + cursor: default; + opacity: 0.1; + } + + &-prev { + inset-inline-start: 0; + } + + &-next { + inset-inline-end: 0; + } + } + + // =================== Footer =================== + &-footer { + position: absolute; + bottom: 0; + left: 50%; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + transform: translateX(-50%); + } + + &-progress { + color: #fff; + } } diff --git a/src/Preview/Footer.tsx b/src/Preview/Footer.tsx new file mode 100644 index 00000000..66ba8b44 --- /dev/null +++ b/src/Preview/Footer.tsx @@ -0,0 +1,136 @@ +import classNames from 'classnames'; +import * as React from 'react'; +import type { Actions, PreviewProps } from '.'; + +export interface FooterProps extends Actions { + prefixCls: string; + showProgress: boolean; + countRender?: PreviewProps['countRender']; + actionsRender?: PreviewProps['actionsRender']; + current: number; + count: number; + showSwitch: boolean; +} + +type OperationType = + | 'prev' + | 'next' + | 'flipY' + | 'flipX' + | 'rotateLeft' + | 'rotateRight' + | 'zoomOut' + | 'zoomIn'; + +interface RenderOperationParams { + icon: React.ReactNode; + type: OperationType; + disabled?: boolean; + onClick: (e: React.MouseEvent) => void; +} + +export default function Footer(props: FooterProps) { + const { prefixCls, showProgress, countRender, current, count, showSwitch, onActive } = props; + + // ========================== Render ========================== + // >>>>> Progress + const progressNode = showProgress && ( +
+ {countRender ? countRender(current + 1, count) : `${current + 1} / ${count}`} +
+ ); + + // >>>>> Actions + const actionCls = `${prefixCls}-actions-operation`; + + const renderOperation = ({ type, disabled, onClick, icon }: RenderOperationParams) => { + return ( +
+ {icon} +
+ ); + }; + + const switchPrevNode = showSwitch + ? renderOperation({ + icon: left, + onClick: e => handleActive(e, -1), + type: 'prev', + disabled: current === 0, + }) + : undefined; + + const switchNextNode = showSwitch + ? renderOperation({ + icon: right, + onClick: e => handleActive(e, 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 = ( +
+ {flipYNode} + {flipXNode} + {rotateLeftNode} + {rotateRightNode} + {zoomOutNode} + {zoomInNode} +
+ ); + + return ( +
+ {progressNode} + {actionsNode} +
+ ); +} diff --git a/src/Preview/PrevNext.tsx b/src/Preview/PrevNext.tsx new file mode 100644 index 00000000..adaad514 --- /dev/null +++ b/src/Preview/PrevNext.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +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) { + const { + prefixCls, + onActive, + current, + count, + icons: { left, right, prev, next }, + } = props; + + const switchCls = `${prefixCls}-switch`; + + return ( + <> +
onActive(-1)} + > + {prev ?? left} +
+
onActive(1)} + > + {next ?? right} +
+ + ); +} diff --git a/src/Preview.tsx b/src/Preview/index.tsx similarity index 83% rename from src/Preview.tsx rename to src/Preview/index.tsx index dfb42d23..5f314b67 100644 --- a/src/Preview.tsx +++ b/src/Preview/index.tsx @@ -4,14 +4,44 @@ import Portal from '@rc-component/portal'; import KeyCode from '@rc-component/util/lib/KeyCode'; import classnames from 'classnames'; import React, { useContext, useEffect, useRef, useState } from 'react'; -import type { ImgInfo, SemanticName } from './Image'; -import { PreviewGroupContext } from './context'; -import type { TransformAction, TransformType } from './hooks/useImageTransform'; -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 type { ImgInfo, SemanticName } from '../Image'; +import { PreviewGroupContext } from '../context'; +import type { TransformAction, TransformType } from '../hooks/useImageTransform'; +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 Footer from './Footer'; +import PrevNext from './PrevNext'; + +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: { @@ -24,17 +54,7 @@ export type ToolbarRenderInfoType = { 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; - }; + actions: Actions; transform: TransformType; current: number; total: number; @@ -52,17 +72,7 @@ export interface PreviewProps extends Omit React.ReactNode; onClose?: () => void; onTransform?: (info: { transform: TransformType; action: TransformAction }) => void; - toolbarRender?: ( + actionsRender?: ( originalNode: React.ReactElement, info: ToolbarRenderInfoType, ) => React.ReactNode; - onChange?: (current, prev) => void; + onChange?: (current: number, prev: number) => void; classNames?: Partial>; styles?: Partial> & { /** Temporarily used in PurePanel, not used externally by antd */ @@ -135,7 +145,7 @@ const Preview: React.FC = props => { maskTransitionName = 'fade', imageRender, imgCommonProps, - toolbarRender, + actionsRender, onTransform, onChange, classNames: imageClassNames, @@ -328,6 +338,7 @@ const Preview: React.FC = props => { className={classnames(`${prefixCls}-body`, bodyMotionClassName)} style={bodyMotionStyle} > + {/* Preview Image */} {imageRender ? imageRender(imgNode, { transform, @@ -335,6 +346,17 @@ const Preview: React.FC = props => { ...(groupContext ? { current } : {}), }) : imgNode} + + {/* Switch prev or next */} + {showLeftOrRightSwitches && ( + + )}
); @@ -363,6 +385,16 @@ const Preview: React.FC = props => { // ); }} +
)} From f88756ce20e7e8c9612ac43c14c083642ae51845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 12 Mar 2025 19:43:42 +0800 Subject: [PATCH 04/25] chore: tmp of it --- assets/preview.less | 22 ++++++++- src/Preview/Footer.tsx | 108 +++++++++++++++++++++++++++++++++-------- src/Preview/index.tsx | 21 +++++++- 3 files changed, 127 insertions(+), 24 deletions(-) diff --git a/assets/preview.less b/assets/preview.less index 907d62b9..2f156e78 100644 --- a/assets/preview.less +++ b/assets/preview.less @@ -67,11 +67,12 @@ // =================== Footer =================== &-footer { position: absolute; - bottom: 0; + bottom: 24px; left: 50%; z-index: 1; display: flex; flex-direction: column; + gap: 16px; align-items: center; transform: translateX(-50%); } @@ -79,4 +80,23 @@ &-progress { color: #fff; } + + // =================== Action =================== + &-actions { + display: flex; + gap: 8px; + padding: 8px 16px; + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + + &-action { + color: #fff; + cursor: pointer; + + &-disabled { + cursor: default; + opacity: 0.5; + } + } + } } diff --git a/src/Preview/Footer.tsx b/src/Preview/Footer.tsx index 66ba8b44..2a9fb53a 100644 --- a/src/Preview/Footer.tsx +++ b/src/Preview/Footer.tsx @@ -1,16 +1,8 @@ import classNames from 'classnames'; import * as React from 'react'; import type { Actions, PreviewProps } from '.'; - -export interface FooterProps extends Actions { - prefixCls: string; - showProgress: boolean; - countRender?: PreviewProps['countRender']; - actionsRender?: PreviewProps['actionsRender']; - current: number; - count: number; - showSwitch: boolean; -} +import type { ImgInfo } from '../Image'; +import type { TransformType } from '../hooks/useImageTransform'; type OperationType = | 'prev' @@ -29,8 +21,56 @@ interface RenderOperationParams { onClick: (e: React.MouseEvent) => void; } +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; +} + export default function Footer(props: FooterProps) { - const { prefixCls, showProgress, countRender, current, count, showSwitch, onActive } = props; + // 修改解构,添加缺失的属性,并提供默认值 + const { + prefixCls, + showProgress, + current, + count, + showSwitch, + + // 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 @@ -41,7 +81,7 @@ export default function Footer(props: FooterProps) { ); // >>>>> Actions - const actionCls = `${prefixCls}-actions-operation`; + const actionCls = `${prefixCls}-actions-action`; const renderOperation = ({ type, disabled, onClick, icon }: RenderOperationParams) => { return ( @@ -59,8 +99,8 @@ export default function Footer(props: FooterProps) { const switchPrevNode = showSwitch ? renderOperation({ - icon: left, - onClick: e => handleActive(e, -1), + icon: prev ?? left, + onClick: () => onActive(-1), type: 'prev', disabled: current === 0, }) @@ -68,8 +108,8 @@ export default function Footer(props: FooterProps) { const switchNextNode = showSwitch ? renderOperation({ - icon: right, - onClick: e => handleActive(e, 1), + icon: next ?? right, + onClick: () => onActive(1), type: 'next', disabled: current === count - 1, }) @@ -114,10 +154,7 @@ export default function Footer(props: FooterProps) { }); const actionsNode = ( -
+
{flipYNode} {flipXNode} {rotateLeftNode} @@ -127,10 +164,39 @@ export default function Footer(props: FooterProps) {
); + // >>>>> Render return (
{progressNode} - {actionsNode} + {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}
); } diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index 5f314b67..df62bb6d 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -388,12 +388,29 @@ const Preview: React.FC = props => {
From 3079bd400e2695b7bc4ca9f89145f8bd6b057c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 12 Mar 2025 20:08:00 +0800 Subject: [PATCH 05/25] chore: keys --- assets/preview.less | 15 ++ src/Preview/CloseBtn.tsx | 21 +++ src/Preview/index.tsx | 339 ++++++++++++++++++--------------------- 3 files changed, 193 insertions(+), 182 deletions(-) create mode 100644 src/Preview/CloseBtn.tsx diff --git a/assets/preview.less b/assets/preview.less index 2f156e78..c7840c6a 100644 --- a/assets/preview.less +++ b/assets/preview.less @@ -34,6 +34,21 @@ max-height: 70%; } + // =================== Close ==================== + &-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; + } + // =================== Switch =================== &-switch { position: absolute; diff --git a/src/Preview/CloseBtn.tsx b/src/Preview/CloseBtn.tsx new file mode 100644 index 00000000..d70ea166 --- /dev/null +++ b/src/Preview/CloseBtn.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +export interface CloseBtnProps { + prefixCls: string; + icon?: React.ReactNode; + onClick: React.MouseEventHandler; +} + +export default function CloseBtn(props: CloseBtnProps) { + const { prefixCls, icon, onClick } = props; + + if (icon === false) { + return null; + } + + return ( + + ); +} diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index df62bb6d..b5f2dcae 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -1,6 +1,6 @@ -import type { DialogProps as IDialogPropTypes } from '@rc-component/dialog'; import CSSMotion from '@rc-component/motion'; -import Portal from '@rc-component/portal'; +import Portal, { type PortalProps } from '@rc-component/portal'; +import { useEvent } from '@rc-component/util'; import KeyCode from '@rc-component/util/lib/KeyCode'; import classnames from 'classnames'; import React, { useContext, useEffect, useRef, useState } from 'react'; @@ -12,6 +12,7 @@ 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'; @@ -61,7 +62,7 @@ export type ToolbarRenderInfoType = { image: ImgInfo; }; -export interface PreviewProps extends Omit { +export interface PreviewProps { imgCommonProps?: React.ImgHTMLAttributes; src?: string; alt?: string; @@ -96,6 +97,20 @@ export interface PreviewProps extends Omit { @@ -141,14 +156,13 @@ const Preview: React.FC = props => { scaleStep = 0.5, minScale = 1, maxScale = 50, - transitionName = 'zoom', - maskTransitionName = 'fade', + motionName = 'fade', imageRender, imgCommonProps, actionsRender, onTransform, onChange, - classNames: imageClassNames, + classNames: classNames, styles, ...restProps } = props; @@ -200,6 +214,55 @@ const Preview: React.FC = props => { } }, [visible]); + const onDoubleClick = (event: React.MouseEvent) => { + if (visible) { + if (scale !== 1) { + updateTransform({ x: 0, y: 0, scale: 1 }, 'doubleClick'); + } else { + dispatchZoomChange( + BASE_SCALE_RATIO + scaleStep, + 'doubleClick', + event.clientX, + event.clientY, + ); + } + } + }; + + const imgNode = ( + + ); + + const image = { + url: src, + alt, + ...imageInfo, + }; + + // ======================== Transform ========================= + // >>>>> Actions const onZoomIn = () => { dispatchZoomChange(BASE_SCALE_RATIO + scaleStep, 'zoomIn'); }; @@ -240,104 +303,63 @@ const Preview: React.FC = props => { onChange?.(position, current); }; - useEffect(() => { - const onGlobalKeyDown = (event: KeyboardEvent) => { - if (!visible) return; + // >>>>> Effect: Keyboard + const onKeyDown = useEvent((event: KeyboardEvent) => { + if (visible) { + const { keyCode } = event; - if (event.keyCode === KeyCode.ESC) { + if (keyCode === KeyCode.ESC) { onClose?.(); } if (showLeftOrRightSwitches) { - if (event.keyCode === KeyCode.LEFT) { + if (keyCode === KeyCode.LEFT) { onActive(-1); - } else if (event.keyCode === KeyCode.RIGHT) { + } else if (keyCode === KeyCode.RIGHT) { onActive(1); } } - }; - - window.addEventListener('keydown', onGlobalKeyDown, false); - - return () => { - window.removeEventListener('keydown', onGlobalKeyDown); - }; - }, [visible, showLeftOrRightSwitches, current]); - - const onDoubleClick = (event: React.MouseEvent) => { - if (visible) { - if (scale !== 1) { - updateTransform({ x: 0, y: 0, scale: 1 }, 'doubleClick'); - } else { - dispatchZoomChange( - BASE_SCALE_RATIO + scaleStep, - 'doubleClick', - event.clientX, - event.clientY, - ); - } } - }; + }); - const imgNode = ( - - ); + useEffect(() => { + if (visible) { + window.addEventListener('keydown', onKeyDown); - const image = { - url: src, - alt, - ...imageInfo, - }; + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + } + }, [visible]); + // ========================== Render ========================== return ( <> {visible && ( -
- - {({ className: maskMotionClassName, style: maskMotionStyle }) => { - return ( + + {({ className: motionClassName, style: motionStyle }) => { + return ( +
+ {/* Mask */}
- ); - }} - - - {({ className: bodyMotionClassName, style: bodyMotionStyle }) => { - return ( -
+ + {/* Body */} +
{/* Preview Image */} {imageRender ? imageRender(imgNode, { @@ -346,107 +368,60 @@ const Preview: React.FC = props => { ...(groupContext ? { current } : {}), }) : imgNode} - - {/* Switch prev or next */} - {showLeftOrRightSwitches && ( - - )}
- ); - - // return ( - //
- //
- //
- //
- // {imageRender - // ? imageRender(imgNode, { - // transform, - // image, - // ...(groupContext ? { current } : {}), - // }) - // : imgNode} - //
- //
- //
- //
- // ); - }} - -
-
+ + {/* Close Button */} + + + {/* Switch prev or next */} + {showLeftOrRightSwitches && ( + + )} + + {/* Footer */} +
+
+ ); + }} + )} - {/* */} ); }; From 00fd944437ec9aa4ede1c32139f8edee878ffe76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 13 Mar 2025 15:23:52 +0800 Subject: [PATCH 06/25] feat: motion auto --- assets/index.less | 90 +++++++++--------- assets/preview.less | 38 +++++++- src/Image.tsx | 6 +- src/Preview/index.tsx | 210 +++++++++++++++++++++++------------------- src/util.ts | 18 ---- 5 files changed, 198 insertions(+), 164 deletions(-) diff --git a/assets/index.less b/assets/index.less index 663bf7cc..1afaa2b5 100644 --- a/assets/index.less +++ b/assets/index.less @@ -253,53 +253,53 @@ // } } -.fade-enter, -.fade-appear { - animation-duration: 0.3s; - animation-play-state: paused; - animation-fill-mode: both; -} -.fade-leave { - animation-duration: 0.3s; - animation-play-state: paused; - animation-fill-mode: both; -} -.fade-enter.fade-enter-active, -.fade-appear.fade-appear-active { - animation-name: rcImageFadeIn; - animation-play-state: running; -} -.fade-leave.fade-leave-active { - animation-name: rcImageFadeOut; - animation-play-state: running; - pointer-events: none; -} -.fade-enter, -.fade-appear { - opacity: 0; - animation-timing-function: linear; -} -.fade-leave { - animation-timing-function: linear; -} +// .fade-enter, +// .fade-appear { +// animation-duration: 0.3s; +// animation-play-state: paused; +// animation-fill-mode: both; +// } +// .fade-leave { +// animation-duration: 0.3s; +// animation-play-state: paused; +// animation-fill-mode: both; +// } +// .fade-enter.fade-enter-active, +// .fade-appear.fade-appear-active { +// animation-name: rcImageFadeIn; +// animation-play-state: running; +// } +// .fade-leave.fade-leave-active { +// animation-name: rcImageFadeOut; +// animation-play-state: running; +// pointer-events: none; +// } +// .fade-enter, +// .fade-appear { +// opacity: 0; +// animation-timing-function: linear; +// } +// .fade-leave { +// animation-timing-function: linear; +// } -@keyframes rcImageFadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} +// @keyframes rcImageFadeIn { +// 0% { +// opacity: 0; +// } +// 100% { +// opacity: 1; +// } +// } -@keyframes rcImageFadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} +// @keyframes rcImageFadeOut { +// 0% { +// opacity: 1; +// } +// 100% { +// opacity: 0; +// } +// } // .zoom-enter, // .zoom-appear { diff --git a/assets/preview.less b/assets/preview.less index c7840c6a..dc7eff3f 100644 --- a/assets/preview.less +++ b/assets/preview.less @@ -1,9 +1,5 @@ @import (reference) 'index.less'; -.zoom { - transition: opacity 0.3s; -} - .@{prefixCls}-preview { position: fixed; overflow: hidden; @@ -114,4 +110,38 @@ } } } + + // =================== Motion =================== + &.fade { + // Basic fade + transition: all 0.3s; + + // Fade in + &-enter, + &-appear { + opacity: 0; + + .@{prefixCls}-preview-body { + transform: scale(0); + } + + &-active { + opacity: 1; + + .@{prefixCls}-preview-body { + transform: scale(1); + transition: all 0.3s; + } + } + } + + // Fade out + &-leave { + opacity: 1; + + &-active { + opacity: 0; + } + } + } } diff --git a/src/Image.tsx b/src/Image.tsx index 05bac563..4af21e0d 100644 --- a/src/Image.tsx +++ b/src/Image.tsx @@ -13,7 +13,6 @@ import type { TransformType } from './hooks/useImageTransform'; import useRegisterImage from './hooks/useRegisterImage'; import useStatus from './hooks/useStatus'; import type { ImageElementProps } from './interface'; -import { getOffset } from './util'; export interface ImgInfo { url: string; @@ -171,7 +170,10 @@ const ImageInternal: CompoundedComponent = props => { // ========================== Preview =========================== const onPreview: React.MouseEventHandler = e => { - const { left, top } = getOffset(e.target as HTMLDivElement); + const rect = (e.target as HTMLDivElement).getBoundingClientRect(); + const left = rect.x + rect.width / 2; + const top = rect.y + rect.height / 2; + if (groupContext) { groupContext.onPreview(imageId, src, left, top); } else { diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index b5f2dcae..95f2dae0 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -107,6 +107,7 @@ export interface PreviewProps { // Motion motionName: string; + mousePosition: null | { x: number; y: number }; // Image width?: string | number; @@ -164,13 +165,15 @@ const Preview: React.FC = props => { onChange, classNames: classNames, styles, - ...restProps + mousePosition, } = props; const imgRef = useRef(); 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, @@ -198,10 +201,6 @@ const Preview: React.FC = props => { ); const { rotate, scale } = transform; - const wrapClassName = classnames({ - [`${prefixCls}-moving`]: isMoving, - }); - useEffect(() => { if (!enableTransition) { setEnableTransition(true); @@ -214,6 +213,7 @@ const Preview: React.FC = props => { } }, [visible]); + // ========================== Image =========================== const onDoubleClick = (event: React.MouseEvent) => { if (visible) { if (scale !== 1) { @@ -261,7 +261,7 @@ const Preview: React.FC = props => { ...imageInfo, }; - // ======================== Transform ========================= + // ======================== Operation ========================= // >>>>> Actions const onZoomIn = () => { dispatchZoomChange(BASE_SCALE_RATIO + scaleStep, 'zoomIn'); @@ -332,97 +332,117 @@ const Preview: React.FC = props => { } }, [visible]); + // ======================= Lock Scroll ======================== + const [lockScroll, setLockScroll] = useState(false); + + React.useEffect(() => { + if (visible) { + setLockScroll(true); + } + }, [visible]); + + const onVisibleChanged = (nextVisible: boolean) => { + if (!nextVisible) { + setLockScroll(false); + } + }; + // ========================== Render ========================== + const bodyStyle: React.CSSProperties = {}; + if (mousePosition) { + bodyStyle.transformOrigin = `${mousePosition.x}px ${mousePosition.y}px`; + } + return ( - <> - {visible && ( - - - {({ className: motionClassName, style: motionStyle }) => { - return ( -
- {/* Mask */} -
- - {/* Body */} -
- {/* Preview Image */} - {imageRender - ? imageRender(imgNode, { - transform, - image, - ...(groupContext ? { current } : {}), - }) - : imgNode} -
- - {/* Close Button */} - - - {/* Switch prev or next */} - {showLeftOrRightSwitches && ( - - )} - - {/* Footer */} -
-
- ); - }} - - - )} - + + + {({ className: motionClassName, style: motionStyle }) => { + return ( +
+ {/* Mask */} +
+ + {/* Body */} +
+ {/* Preview Image */} + {imageRender + ? imageRender(imgNode, { + transform, + image, + ...(groupContext ? { current } : {}), + }) + : imgNode} +
+ + {/* Close Button */} + + + {/* Switch prev or next */} + {showLeftOrRightSwitches && ( + + )} + + {/* Footer */} +
+
+ ); + }} + + ); }; diff --git a/src/util.ts b/src/util.ts index 8a407809..9f44fef2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -8,24 +8,6 @@ export function isImageValid(src: string) { } // ============================= Legacy ============================= -// TODO: Remove this. It's copy directly from legacy `rc-util` package -export function getOffset(node: HTMLElement) { - const box = node.getBoundingClientRect(); - const docElem = document.documentElement; - - // < ie8 不支持 win.pageXOffset, 则使用 docElem.scrollLeft - return { - left: - box.left + - (window.pageXOffset || docElem.scrollLeft) - - (docElem.clientLeft || document.body.clientLeft || 0), - top: - box.top + - (window.pageYOffset || docElem.scrollTop) - - (docElem.clientTop || document.body.clientTop || 0), - }; -} - export function getClientSize() { const width = document.documentElement.clientWidth; const height = window.innerHeight || document.documentElement.clientHeight; From 39703c615947217e231fa7ceaab02392a2c42384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 13 Mar 2025 15:25:24 +0800 Subject: [PATCH 07/25] chore: fix ts --- src/Image.tsx | 6 +- src/Operations.tsx | 297 ------------------------------------------ src/Preview/index.tsx | 2 +- src/PreviewGroup.tsx | 8 +- 4 files changed, 8 insertions(+), 305 deletions(-) delete mode 100644 src/Operations.tsx diff --git a/src/Image.tsx b/src/Image.tsx index 4af21e0d..85dc1e89 100644 --- a/src/Image.tsx +++ b/src/Image.tsx @@ -44,7 +44,7 @@ export interface ImagePreviewType info: { transform: TransformType; image: ImgInfo }, ) => React.ReactNode; onTransform?: PreviewProps['onTransform']; - toolbarRender?: ( + actionsRender?: ( originalNode: React.ReactElement, info: Omit, ) => React.ReactNode; @@ -114,7 +114,7 @@ const ImageInternal: CompoundedComponent = props => { minScale, maxScale, imageRender, - toolbarRender, + actionsRender, ...dialogProps }: ImagePreviewType = typeof preview === 'object' ? preview : {}; const src = previewSrc ?? imgSrc; @@ -259,7 +259,7 @@ const ImageInternal: CompoundedComponent = props => { rootClassName={rootClassName} imageRender={imageRender} imgCommonProps={imgCommonProps} - toolbarRender={toolbarRender} + actionsRender={actionsRender} classNames={imageClassNames} styles={styles} {...dialogProps} diff --git a/src/Operations.tsx b/src/Operations.tsx deleted file mode 100644 index ce6aa2c5..00000000 --- a/src/Operations.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import Portal from '@rc-component/portal'; -import classnames from 'classnames'; -import CSSMotion from '@rc-component/motion'; -import KeyCode from '@rc-component/util/lib/KeyCode'; -import * as React from 'react'; -import { useContext } from 'react'; -import type { ImgInfo, SemanticName } from './Image'; -import type { PreviewProps, ToolbarRenderInfoType } from './Preview'; -import { PreviewGroupContext } from './context'; -import type { TransformType } from './hooks/useImageTransform'; -import classNames from 'classnames'; - -type OperationType = - | 'prev' - | 'next' - | 'flipY' - | 'flipX' - | 'rotateLeft' - | 'rotateRight' - | 'zoomOut' - | 'zoomIn'; - -interface RenderOperationParams { - icon: React.ReactNode; - type: OperationType; - disabled?: boolean; - onClick: (e: React.MouseEvent) => void; -} - -interface OperationsProps - extends Pick< - PreviewProps, - | 'visible' - | 'maskTransitionName' - | 'getContainer' - | 'prefixCls' - | 'rootClassName' - | 'icons' - | 'countRender' - | 'closeIcon' - | 'onClose' - > { - showSwitch: boolean; - showProgress: boolean; - current: number; - transform: TransformType; - count: number; - scale: number; - minScale: number; - maxScale: number; - onActive: (offset: number) => void; - onZoomIn: () => void; - onZoomOut: () => void; - onRotateRight: () => void; - onRotateLeft: () => void; - onFlipX: () => void; - onFlipY: () => void; - onReset: () => void; - toolbarRender: ( - originalNode: React.ReactElement, - info: ToolbarRenderInfoType | Omit, - ) => React.ReactNode; - zIndex?: number; - image?: ImgInfo; - classNames?: Partial>; - styles?: Partial>; -} - -const Operations: React.FC = props => { - const { - visible, - maskTransitionName, - getContainer, - prefixCls, - rootClassName, - icons, - countRender, - showSwitch, - showProgress, - current, - transform, - count, - scale, - minScale, - maxScale, - closeIcon, - onActive, - onClose, - onZoomIn, - onZoomOut, - onRotateRight, - onRotateLeft, - onFlipX, - onFlipY, - onReset, - toolbarRender, - zIndex, - image, - classNames: imageClassNames, - styles, - } = props; - const groupContext = useContext(PreviewGroupContext); - const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right, flipX, flipY } = icons; - const toolClassName = `${prefixCls}-operations-operation`; - - React.useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.keyCode === KeyCode.ESC) { - onClose(); - } - }; - - if (visible) { - window.addEventListener('keydown', onKeyDown); - } - - return () => { - window.removeEventListener('keydown', onKeyDown); - }; - }, [visible]); - - const handleActive = (e: React.MouseEvent, offset: number) => { - e.preventDefault(); - e.stopPropagation(); - - onActive(offset); - }; - - const renderOperation = React.useCallback( - ({ type, disabled, onClick, icon }: RenderOperationParams) => { - return ( -
- {icon} -
- ); - }, - [toolClassName, prefixCls], - ); - - const switchPrevNode = showSwitch - ? renderOperation({ - icon: left, - onClick: e => handleActive(e, -1), - type: 'prev', - disabled: current === 0, - }) - : undefined; - - const switchNextNode = showSwitch - ? renderOperation({ - icon: right, - onClick: e => handleActive(e, 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 toolbarNode = ( -
- {flipYNode} - {flipXNode} - {rotateLeftNode} - {rotateRightNode} - {zoomOutNode} - {zoomInNode} -
- ); - - return ( - - {({ className, style }) => ( - -
- {closeIcon === null ? null : ( - - )} - - {showSwitch && ( - <> -
handleActive(e, -1)} - > - {left} -
-
handleActive(e, 1)} - > - {right} -
- - )} - -
- {showProgress && ( -
- {countRender ? countRender(current + 1, count) : `${current + 1} / ${count}`} -
- )} - - {toolbarRender - ? toolbarRender(toolbarNode, { - 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, - ...(groupContext ? { current, total: count } : {}), - image, - }) - : toolbarNode} -
-
-
- )} -
- ); -}; - -export default Operations; diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index 95f2dae0..c6b8bf11 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -106,7 +106,7 @@ export interface PreviewProps { getContainer?: PortalProps['getContainer']; // Motion - motionName: string; + motionName?: string; mousePosition: null | { x: number; y: number }; // Image diff --git a/src/PreviewGroup.tsx b/src/PreviewGroup.tsx index 5fc0bc90..92df2cf5 100644 --- a/src/PreviewGroup.tsx +++ b/src/PreviewGroup.tsx @@ -1,7 +1,7 @@ import useMergedState from '@rc-component/util/lib/hooks/useMergedState'; import * as React from 'react'; import { useState } from 'react'; -import type { ImgInfo, ImagePreviewType } from './Image'; +import type { ImagePreviewType, ImgInfo } from './Image'; import type { PreviewProps, ToolbarRenderInfoType } from './Preview'; import Preview from './Preview'; import { PreviewGroupContext } from './context'; @@ -20,7 +20,7 @@ export interface PreviewGroupPreview */ current?: number; countRender?: (current: number, total: number) => React.ReactNode; - toolbarRender?: ( + actionsRender?: ( originalNode: React.ReactElement, info: ToolbarRenderInfoType, ) => React.ReactNode; @@ -61,7 +61,7 @@ const Group: React.FC = ({ closeIcon, onChange, onTransform, - toolbarRender, + actionsRender, imageRender, ...dialogProps } = typeof preview === 'object' ? preview : ({} as PreviewGroupPreview); @@ -158,7 +158,7 @@ const Group: React.FC = ({ count={mergedItems.length} countRender={countRender} onTransform={onTransform} - toolbarRender={toolbarRender} + actionsRender={actionsRender} imageRender={imageRender} onChange={onInternalChange} {...dialogProps} From 7e8df79028341d1a840d085cb48000addbcc4b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 13 Mar 2025 15:33:09 +0800 Subject: [PATCH 08/25] chore: fix ts --- src/PreviewGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PreviewGroup.tsx b/src/PreviewGroup.tsx index 92df2cf5..70c19507 100644 --- a/src/PreviewGroup.tsx +++ b/src/PreviewGroup.tsx @@ -12,7 +12,7 @@ import type { ImageElementProps, OnGroupPreview } from './interface'; export interface PreviewGroupPreview extends Omit< ImagePreviewType, - 'mask' | 'maskClassName' | 'onVisibleChange' | 'toolbarRender' | 'imageRender' + 'mask' | 'maskClassName' | 'onVisibleChange' | 'actionsRender' | 'imageRender' > { /** * If Preview the show img index From da34b7fc61ebdaf23e2eca3754580399fdb298ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 13 Mar 2025 15:34:21 +0800 Subject: [PATCH 09/25] docs: update --- README.md | 4 ++-- docs/demo/actionsRender.md | 8 ++++++++ docs/demo/toolbarRender.md | 8 -------- docs/examples/{toolbarRender.tsx => actionsRender.tsx} | 4 ++-- docs/examples/imageRender.tsx | 2 +- tests/preview.test.tsx | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 docs/demo/actionsRender.md delete mode 100644 docs/demo/toolbarRender.md rename docs/examples/{toolbarRender.tsx => actionsRender.tsx} (96%) diff --git a/README.md b/README.md index 9c6d4fdf..94450a9d 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ export default () => ( | 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 | -| toolbarRender | (originalNode: React.ReactElement, info: Omit<[ToolbarRenderInfoType](#ToolbarRenderInfoType), 'current' \| 'total'>) => React.ReactNode | - | Customize toolbar | +| actionsRender | (originalNode: React.ReactElement, info: Omit<[ToolbarRenderInfoType](#ToolbarRenderInfoType), 'current' \| 'total'>) => React.ReactNode | - | Customize toolbar | | onVisibleChange | (visible: boolean, prevVisible: boolean) => void | - | Callback when visible is changed | | onTransform | { transform: [TransformType](#TransformType), action: [TransformAction](#TransformAction) } | - | Callback when transform is changed | @@ -131,7 +131,7 @@ export default () => ( | 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 | -| toolbarRender | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#ToolbarRenderInfoType)) => React.ReactNode | - | Customize toolbar | +| actionsRender | (originalNode: React.ReactElement, info: [ToolbarRenderInfoType](#ToolbarRenderInfoType)) => React.ReactNode | - | Customize toolbar | | onVisibleChange | (visible: boolean, prevVisible: boolean, current: number) => void | - | Callback when visible is changed | | onTransform | { transform: [TransformType](#TransformType), action: [TransformAction](#TransformAction) } | - | Callback when transform is changed | diff --git a/docs/demo/actionsRender.md b/docs/demo/actionsRender.md new file mode 100644 index 00000000..6a6cd676 --- /dev/null +++ b/docs/demo/actionsRender.md @@ -0,0 +1,8 @@ +--- +title: actionsRender +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/demo/toolbarRender.md b/docs/demo/toolbarRender.md deleted file mode 100644 index e5f21be7..00000000 --- a/docs/demo/toolbarRender.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: toolbarRender -nav: - title: Demo - path: /demo ---- - - diff --git a/docs/examples/toolbarRender.tsx b/docs/examples/actionsRender.tsx similarity index 96% rename from docs/examples/toolbarRender.tsx rename to docs/examples/actionsRender.tsx index 8ee8214f..1b9e1b84 100644 --- a/docs/examples/toolbarRender.tsx +++ b/docs/examples/actionsRender.tsx @@ -11,7 +11,7 @@ export default function ToolbarRender() { width={200} preview={{ icons: defaultIcons, - toolbarRender: ( + actionsRender: ( _, { actions: { @@ -55,7 +55,7 @@ export default function ToolbarRender() { imageRender(_, { image }) { return
{JSON.stringify(image)}
; }, - toolbarRender(_, { image }) { + actionsRender(_, { image }) { return
{JSON.stringify(image)}
; }, }} diff --git a/docs/examples/imageRender.tsx b/docs/examples/imageRender.tsx index afea3c05..14c0b844 100644 --- a/docs/examples/imageRender.tsx +++ b/docs/examples/imageRender.tsx @@ -11,7 +11,7 @@ export default function imageRender() { width={200} preview={{ icons: defaultIcons, - toolbarRender: () => null, + actionsRender: () => null, imageRender: () => (