From 09ecbc2be6d186273da65f3eb1b318825cd629ea Mon Sep 17 00:00:00 2001 From: infeng Date: Thu, 29 Aug 2019 14:38:35 +0800 Subject: [PATCH] refactor: hooks --- demo/index.tsx | 12 +- package-lock.json | 59 ++- package.json | 8 +- src/Loading.tsx | 20 +- src/Viewer copy.tsx | 96 ++++ src/Viewer.tsx | 107 +---- src/ViewerCanvas.tsx | 228 ++++----- src/ViewerCore copy.tsx | 679 ++++++++++++++++++++++++++ src/ViewerCore.tsx | 1015 ++++++++++++++++++++------------------- src/ViewerNav.tsx | 54 +-- src/ViewerToolbar.tsx | 86 ++-- 11 files changed, 1552 insertions(+), 812 deletions(-) create mode 100644 src/Viewer copy.tsx create mode 100644 src/ViewerCore copy.tsx diff --git a/demo/index.tsx b/demo/index.tsx index c2f7d68..202c603 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -20,8 +20,8 @@ interface State { class App extends React.Component> { container: HTMLDivElement; - constructor() { - super(); + constructor(props) { + super(props); this.state = { visible: false, @@ -121,12 +121,14 @@ class App extends React.Component> { ); })} -
{this.container = ref;}}>
+
{this.container = ref; }}>
{ this.setState({ visible: false }); } } + onClose={() => { + this.setState({ visible: false }); + }} images={images} activeIndex={this.state.activeIndex} container={inline ? this.container : null} @@ -154,5 +156,5 @@ class App extends React.Component> { ReactDOM.render( , - document.getElementById('root') + document.getElementById('root'), ); diff --git a/package-lock.json b/package-lock.json index d811d30..fb8d31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,16 +123,26 @@ "integrity": "sha512-vToa8YEeulfyYg1gSOeHjvvIRqrokng62VMSj2hoZrwZNcYrp2h3AWo6KeBVuymIklQUaY5zgVJvVsC4KiiLkQ==", "dev": true }, - "@types/react": { - "version": "0.14.57", - "resolved": "https://registry.npmjs.org/@types/react/-/react-0.14.57.tgz", - "integrity": "sha1-GHioZU+v3R04G4RXKStkM0mMW2I=", + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npm.taobao.org/@types/prop-types/download/@types/prop-types-15.7.1.tgz", + "integrity": "sha1-8aEee6uww8rWgQC+OB0eBkxo8fY=", "dev": true }, + "@types/react": { + "version": "16.9.1", + "resolved": "https://registry.npm.taobao.org/@types/react/download/@types/react-16.9.1.tgz?cache=0&sync_timestamp=1565360509473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-16.9.1.tgz", + "integrity": "sha1-hiyDtMnVzRFuQv2aTzaUhDzSwFE=", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@types/react-dom": { - "version": "0.14.23", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-0.14.23.tgz", - "integrity": "sha1-zs/PrXVLTCdl/l0puBswGImtbC4=", + "version": "16.8.5", + "resolved": "https://registry.npm.taobao.org/@types/react-dom/download/@types/react-dom-16.8.5.tgz", + "integrity": "sha1-Pj9NmRmTkaf7QKo6FVyN2ZuJnL0=", "dev": true, "requires": { "@types/react": "*" @@ -2840,6 +2850,12 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npm.taobao.org/csstype/download/csstype-2.6.6.tgz", + "integrity": "sha1-w0+CJqlLuxDDLMDXFK/flCKR/EE=", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -11119,27 +11135,26 @@ } }, "react": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz", - "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==", + "version": "16.9.0", + "resolved": "https://registry.npm.taobao.org/react/download/react-16.9.0.tgz?cache=0&sync_timestamp=1565317868688&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact%2Fdownload%2Freact-16.9.0.tgz", + "integrity": "sha1-QLovmvE7waONddvy9DWaUYXE96o=", "dev": true, "requires": { - "fbjs": "^0.8.16", "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "prop-types": "^15.6.2" } }, "react-dom": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.2.tgz", - "integrity": "sha512-Usl73nQqzvmJN+89r97zmeUpQDKDlh58eX6Hbs/ERdDHzeBzWy+ENk7fsGQ+5KxArV1iOFPT46/VneklK9zoWw==", + "version": "16.9.0", + "resolved": "https://registry.npm.taobao.org/react-dom/download/react-dom-16.9.0.tgz", + "integrity": "sha1-XmVSel4m8irjcBExvMyu6fsNOWI=", "dev": true, "requires": { - "fbjs": "^0.8.16", "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "prop-types": "^15.6.2", + "scheduler": "^0.15.0" } }, "react-is": { @@ -12143,6 +12158,16 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "scheduler": { + "version": "0.15.0", + "resolved": "https://registry.npm.taobao.org/scheduler/download/scheduler-0.15.0.tgz?cache=0&sync_timestamp=1565317903688&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fscheduler%2Fdownload%2Fscheduler-0.15.0.tgz", + "integrity": "sha1-a/z4D/hQsoD+1K7sxlE7wLTxf44=", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", diff --git a/package.json b/package.json index 799f393..1c61525 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "devDependencies": { "@types/jest": "^23.3.1", "@types/node": "^6.0.45", - "@types/react": "^0.14.39", - "@types/react-dom": "^0.14.17", + "@types/react": "^16.9.1", + "@types/react-dom": "^16.8.5", "antd": "^3.16.2", "atool-build": "^1.0.8", "babel-jest": "^23.4.2", @@ -61,8 +61,8 @@ "jest-static-stubs": "0.0.1", "merge2": "^1.0.2", "pre-commit": "^1.1.3", - "react": "^16.2.0", - "react-dom": "^16.2.0", + "react": "^16.9.0", + "react-dom": "^16.9.0", "react-test-render": "^1.1.1", "through2": "^2.0.1", "ts-jest": "^23.1.3", diff --git a/src/Loading.tsx b/src/Loading.tsx index 11628ed..ef635cb 100644 --- a/src/Loading.tsx +++ b/src/Loading.tsx @@ -4,18 +4,12 @@ export interface LoadingProps { style?: React.CSSProperties; } -export default class Loading extends React.Component { - constructor() { - super(); - } - - render() { - let cls = 'circle-loading'; - return ( -
-
-
+export default function Loading(props: LoadingProps) { + let cls = 'circle-loading'; + return ( +
+
- ); - } +
+ ); } diff --git a/src/Viewer copy.tsx b/src/Viewer copy.tsx new file mode 100644 index 0000000..18709d4 --- /dev/null +++ b/src/Viewer copy.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ViewerCore from './ViewerCore'; +import ViewerProps from './ViewerProps'; + +export default class Viewer extends React.Component { + private defaultContainer: HTMLElement; + private container: HTMLElement; + private component: React.ReactNode; + + constructor(props) { + super(props); + + this.container = null; + this.defaultContainer = null; + if (typeof document !== 'undefined') { + this.setDefaultContainer(); + } + this.component = null; + } + + setDefaultContainer() { + this.defaultContainer = document.createElement('div'); + } + + renderViewer() { + if (this.props.visible || this.component) { + if (!this.container) { + if (this.props.container) { + this.container = this.props.container; + } else { + if (!this.defaultContainer) { + this.setDefaultContainer(); + } + this.container = this.defaultContainer; + document.body.appendChild(this.container); + } + } + let instance = this; + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + , + this.container, + function () { + instance.component = this; + }, + ); + } + } + + removeViewer() { + if (this.container) { + const container = this.container; + ReactDOM.unmountComponentAtNode(container); + container.parentNode.removeChild(container); + this.container = null; + this.component = null; + } + } + + componentWillUnmount() { + if (this.props.visible && this.props.onClose) { + this.props.onClose(); + } + this.removeViewer(); + } + + componentWillReceiveProps(nextProps: ViewerProps) { + if (this.props.container !== nextProps.container) { + this.component = null; + if (nextProps.container) { + if (this.container) { + document.body.removeChild(this.container); + } + this.container = nextProps.container; + } else { + this.container = this.defaultContainer; + document.body.appendChild(this.container); + } + } + } + + componentDidMount() { + this.renderViewer(); + } + + componentDidUpdate() { + this.renderViewer(); + } + + render() { + return null; + } +} diff --git a/src/Viewer.tsx b/src/Viewer.tsx index a26119c..53cc31c 100644 --- a/src/Viewer.tsx +++ b/src/Viewer.tsx @@ -3,94 +3,35 @@ import * as ReactDOM from 'react-dom'; import ViewerCore from './ViewerCore'; import ViewerProps from './ViewerProps'; -export default class Viewer extends React.Component { - private defaultContainer: HTMLElement; - private container: HTMLElement; - private component: React.ReactNode; +export default React.forwardRef((props: ViewerProps, ref) => { + const defaultContainer = React.useRef(document.createElement('div')); + const [ container, setContainer ] = React.useState(props.container); + const [ init, setInit ] = React.useState(false); - constructor() { - super(); + React.useEffect(() => { + document.body.appendChild(defaultContainer.current); + }, []); - this.container = null; - this.defaultContainer = null; - if (typeof document !== 'undefined') { - this.setDefaultContainer(); + React.useEffect(() => { + if (props.visible && !init) { + setInit(true); } - this.component = null; - } + }, [props.visible, init]); - setDefaultContainer() { - this.defaultContainer = document.createElement('div'); - } - - renderViewer() { - if (this.props.visible || this.component) { - if (!this.container) { - if (this.props.container) { - this.container = this.props.container; - } else { - if (!this.defaultContainer) { - this.setDefaultContainer(); - } - this.container = this.defaultContainer; - document.body.appendChild(this.container); - } - } - let instance = this; - ReactDOM.unstable_renderSubtreeIntoContainer( - this, - , - this.container, - function () { - instance.component = this; - }, - ); + React.useEffect(() => { + if (props.container) { + setContainer(props.container); + } else { + setContainer(defaultContainer.current); } - } + }, [props.container]); - removeViewer() { - if (this.container) { - const container = this.container; - ReactDOM.unmountComponentAtNode(container); - container.parentNode.removeChild(container); - this.container = null; - this.component = null; - } - } - - componentWillUnmount() { - if (this.props.visible && this.props.onClose) { - this.props.onClose(); - } - this.removeViewer(); - } - - componentWillReceiveProps(nextProps: ViewerProps) { - if (this.props.container !== nextProps.container) { - this.component = null; - if (nextProps.container) { - if (this.container) { - document.body.removeChild(this.container); - } - this.container = nextProps.container; - } else { - this.container = this.defaultContainer; - document.body.appendChild(this.container); - } - } - } - - componentDidMount() { - this.renderViewer(); - } - - componentDidUpdate() { - this.renderViewer(); - } - - render() { + if (!init) { return null; } -} + return ReactDOM.createPortal(( + + ), container); +}); diff --git a/src/ViewerCanvas.tsx b/src/ViewerCanvas.tsx index ed878f3..f89e56e 100644 --- a/src/ViewerCanvas.tsx +++ b/src/ViewerCanvas.tsx @@ -28,154 +28,154 @@ export interface ViewerCanvasState { mouseY?: number; } -export default class ViewerCanvas extends React.Component { +export default function ViewerCanvas(props: ViewerCanvasProps) { + const isMouseDown = React.useRef(false); + const prePosition = React.useRef({ + x: 0, + y: 0, + }); + const [ position, setPosition ] = React.useState({ + x: 0, + y: 0, + }); - constructor() { - super(); - - this.state = { - isMouseDown: false, - mouseX: 0, - mouseY: 0, + React.useEffect(() => { + return () => { + bindEvent(true); + bindWindowResizeEvent(true); }; - } + }, []); - componentDidMount() { - if (this.props.drag) { - this.bindEvent(); + React.useEffect(() => { + bindWindowResizeEvent(!props.visible); + }, [props.visible]); + + React.useEffect(() => { + if (props.visible && props.drag) { + bindEvent(); } + if (!props.visible && props.drag) { + handleMouseUp({}); + } + return () => { + bindEvent(true); + }; + }, [props.drag, props.visible]); + + React.useEffect(() => { + let diffX = position.x - prePosition.current.x; + let diffY = position.y - prePosition.current.y; + prePosition.current = { + x: position.x, + y: position.y, + }; + props.onChangeImgState(props.width, props.height, props.top + diffY, props.left + diffX); + }, [position]); + + function handleResize(e) { + props.onResize(); } - handleResize = (e) => { - this.props.onResize(); + function handleCanvasMouseDown(e) { + props.onCanvasMouseDown(e); + handleMouseDown(e); } - handleCanvasMouseDown = (e) => { - this.props.onCanvasMouseDown(e); - this.handleMouseDown(e); - } - - handleMouseDown = (e) => { + function handleMouseDown(e) { if (e.button !== 0) { return; } - if (!this.props.visible || !this.props.drag) { + if (!props.visible || !props.drag) { return; } e.preventDefault(); e.stopPropagation(); - this.setState({ - isMouseDown: true, - mouseX: e.nativeEvent.clientX, - mouseY: e.nativeEvent.clientY, - }); + isMouseDown.current = true; + prePosition.current = { + x: e.nativeEvent.clientX, + y: e.nativeEvent.clientY, + }; } - handleMouseMove = (e) => { - if (this.state.isMouseDown) { - let diffX = e.clientX - this.state.mouseX; - let diffY = e.clientY - this.state.mouseY; - this.setState({ - mouseX: e.clientX, - mouseY: e.clientY, + const handleMouseMove = (e) => { + if (isMouseDown.current) { + setPosition({ + x: e.clientX, + y: e.clientY, }); - this.props.onChangeImgState(this.props.width, this.props.height, this.props.top + diffY, this.props.left + diffX); } + }; + + function handleMouseUp(e) { + isMouseDown.current = false; } - handleMouseUp = (e) => { - this.setState({ - isMouseDown: false, - }); + function bindWindowResizeEvent(remove?: boolean) { + let funcName = 'addEventListener'; + if (remove) { + funcName = 'removeEventListener'; + } + window[funcName]('resize', handleResize, false); } - bindEvent = (remove?: boolean) => { + function bindEvent(remove?: boolean) { let funcName = 'addEventListener'; if (remove) { funcName = 'removeEventListener'; } - document[funcName]('click', this.handleMouseUp, false); - document[funcName]('mousemove', this.handleMouseMove, false); - window[funcName]('resize', this.handleResize, false); + document[funcName]('click', handleMouseUp, false); + document[funcName]('mousemove', handleMouseMove, false); } - componentWillReceiveProps(nextProps: ViewerCanvasProps) { - if (!this.props.visible && nextProps.visible) { - if (nextProps.drag) { - return this.bindEvent(); - } - } - if (this.props.visible && !nextProps.visible) { - this.handleMouseUp({}); - if (nextProps.drag) { - return this.bindEvent(true); - } - } - if (this.props.drag && !nextProps.drag) { - return this.bindEvent(true); - } - if (!this.props.drag && nextProps.drag) { - if (nextProps.visible) { - return this.bindEvent(true); - } - } + let imgStyle: React.CSSProperties = { + width: `${props.width}px`, + height: `${props.height}px`, + transform: ` +translateX(${props.left !== null ? props.left + 'px' : 'aoto'}) translateY(${props.top}px) + rotate(${props.rotate}deg) scaleX(${props.scaleX}) scaleY(${props.scaleY})`, + }; + + const imgClass = classnames(`${props.prefixCls}-image`, { + drag: props.drag, + [`${props.prefixCls}-image-transition`]: !isMouseDown.current, + }); + + let style = { + zIndex: props.zIndex, + }; + + let imgNode = null; + if (props.imgSrc !== '') { + imgNode = ; } - - componentWillUnmount() { - this.bindEvent(true); - } - - render() { - let imgStyle: React.CSSProperties = { - width: `${this.props.width}px`, - height: `${this.props.height}px`, - transform: ` -translateX(${this.props.left !== null ? this.props.left + 'px' : 'aoto'}) translateY(${this.props.top}px) - rotate(${this.props.rotate}deg) scaleX(${this.props.scaleX}) scaleY(${this.props.scaleY})`, - }; - - const imgClass = classnames(`${this.props.prefixCls}-image`, { - drag: this.props.drag, - [`${this.props.prefixCls}-image-transition`]: !this.state.isMouseDown, - }); - - let style = { - zIndex: this.props.zIndex, - }; - - let imgNode = null; - if (this.props.imgSrc !== '') { - imgNode = ; - } - if (this.props.loading) { - imgNode = ( -
- -
- ); - } - - return ( + if (props.loading) { + imgNode = (
- {imgNode} +
); } + + return ( +
+ {imgNode} +
+ ); } diff --git a/src/ViewerCore copy.tsx b/src/ViewerCore copy.tsx new file mode 100644 index 0000000..6b86512 --- /dev/null +++ b/src/ViewerCore copy.tsx @@ -0,0 +1,679 @@ +import * as React from 'react'; +import './style/index.less'; +import ViewerCanvas from './ViewerCanvas'; +import ViewerNav from './ViewerNav'; +import ViewerToolbar, { defaultToolbars } from './ViewerToolbar'; +import ViewerProps, { ImageDecorator, ToolbarConfig } from './ViewerProps'; +import Icon, { ActionType } from './Icon'; +import * as constants from './constants'; + +function noop() { } + +const transitionDuration = 300; + +export interface ViewerCoreState { + visible?: boolean; + visibleStart?: boolean; + transitionEnd?: boolean; + activeIndex?: number; + width?: number; + height?: number; + top?: number; + left?: number; + rotate?: number; + imageWidth?: number; + imageHeight?: number; + scaleX?: number; + scaleY?: number; + loading?: boolean; + loadFailed?: boolean; +} + +export default class ViewerCore extends React.Component { + static defaultProps: Partial = { + visible: false, + onClose: noop, + images: [], + activeIndex: 0, + zIndex: 1000, + drag: true, + attribute: true, + zoomable: true, + rotatable: true, + scalable: true, + onMaskClick: noop, + changeable: true, + customToolbar: (toolbars) => toolbars, + zoomSpeed: .05, + disableKeyboardSupport: false, + noResetZoomAfterChange: false, + noLimitInitializationSize: false, + defaultScale: 1, + loop: true, + disableMouseZoom: false, + }; + + private prefixCls: string; + private containerWidth: number; + private containerHeight: number; + private footerHeight: number; + + constructor(props) { + super(props); + + this.prefixCls = 'react-viewer'; + + this.state = { + visible: false, + visibleStart: false, + transitionEnd: false, + activeIndex: this.props.activeIndex, + width: 0, + height: 0, + top: 15, + left: null, + rotate: 0, + imageWidth: 0, + imageHeight: 0, + scaleX: this.props.defaultScale, + scaleY: this.props.defaultScale, + loading: false, + loadFailed: false, + }; + + this.setContainerWidthHeight(); + this.footerHeight = constants.FOOTER_HEIGHT; + } + + setContainerWidthHeight() { + this.containerWidth = window.innerWidth; + this.containerHeight = window.innerHeight; + if (this.props.container) { + this.containerWidth = this.props.container.offsetWidth; + this.containerHeight = this.props.container.offsetHeight; + this.setInlineContainerHeight(); + } + } + + setInlineContainerHeight() { + const core = (this.refs['viewerCore'] as HTMLDivElement); + if (core) { + this.containerHeight = core.offsetHeight; + } + } + + handleClose = () => { + this.props.onClose(); + } + + startVisible(activeIndex: number) { + if (!this.props.container) { + document.body.style.overflow = 'hidden'; + if (document.body.scrollHeight > document.body.clientHeight) { + document.body.style.paddingRight = '15px'; + } + } + this.setState({ + visibleStart: true, + }); + setTimeout(() => { + this.setState({ + visible: true, + activeIndex, + }); + setTimeout(() => { + this.bindEvent(); + this.loadImg(activeIndex); + }, 300); + }, 10); + } + + componentDidMount() { + const core = (this.refs['viewerCore'] as HTMLDivElement); + core.addEventListener( + 'transitionend', + this.handleTransitionEnd, + false, + ); + // Though onWheel can be directly used on the div "viewerCore", to be able to + // prevent default action, a listener is added here instead + (this.refs['viewerCore'] as HTMLDivElement).addEventListener( + 'wheel', + this.handleMouseScroll, + false, + ); + if (this.containerHeight === 0) { + this.setInlineContainerHeight(); + } + this.startVisible(this.state.activeIndex); + } + + getImgWidthHeight(imgWidth, imgHeight) { + let width = 0; + let height = 0; + let maxWidth = this.containerWidth * 0.8; + let maxHeight = (this.containerHeight - this.footerHeight) * 0.8; + width = Math.min(maxWidth, imgWidth); + height = width / imgWidth * imgHeight; + if (height > maxHeight) { + height = maxHeight; + width = height / imgHeight * imgWidth; + } + if (this.props.noLimitInitializationSize) { + width = imgWidth; + height = imgHeight; + } + return [width, height]; + } + + loadImgSuccess = (activeImage: ImageDecorator, imgWidth, imgHeight, isNewImage: boolean) => { + let realImgWidth = imgWidth; + let realImgHeight = imgHeight; + if (this.props.defaultSize) { + realImgWidth = this.props.defaultSize.width; + realImgHeight = this.props.defaultSize.height; + } + if (activeImage.defaultSize) { + realImgWidth = activeImage.defaultSize.width; + realImgHeight = activeImage.defaultSize.height; + } + let [width, height] = this.getImgWidthHeight(realImgWidth, realImgHeight); + let left = (this.containerWidth - width) / 2; + let top = (this.containerHeight - height - this.footerHeight) / 2; + let scaleX = this.props.defaultScale; + let scaleY = this.props.defaultScale; + if (this.props.noResetZoomAfterChange && isNewImage) { + scaleX = this.state.scaleX; + scaleY = this.state.scaleY; + } + this.setState({ + width: width, + height: height, + left: left, + top: top, + imageWidth: imgWidth, + imageHeight: imgHeight, + loading: false, + rotate: 0, + scaleX: scaleX, + scaleY: scaleY, + }); + } + + loadImg(activeIndex, isNewImage: boolean = false) { + let activeImage: ImageDecorator = null; + let images = this.props.images || []; + if (images.length > 0) { + activeImage = images[activeIndex]; + } + let loadComplete = false; + let img = new Image(); + this.setState({ + activeIndex: activeIndex, + loading: true, + loadFailed: false, + }, () => { + img.onload = () => { + if (!loadComplete) { + this.loadImgSuccess(activeImage, img.width, img.height, isNewImage); + } + }; + img.onerror = () => { + if (this.props.defaultImg) { + this.setState({ + loadFailed: true, + }); + const deafultImgWidth = this.props.defaultImg.width || this.containerWidth * .5; + const defaultImgHeight = this.props.defaultImg.height || this.containerHeight * .5; + this.loadImgSuccess(activeImage, deafultImgWidth, defaultImgHeight, isNewImage); + } else { + this.setState({ + activeIndex: activeIndex, + imageWidth: 0, + imageHeight: 0, + loading: false, + }); + } + }; + img.src = activeImage.src; + if (img.complete) { + loadComplete = true; + this.loadImgSuccess(activeImage, img.width, img.height, isNewImage); + } + }); + } + + handleChangeImg = (newIndex: number) => { + if (!this.props.loop && (newIndex >= this.props.images.length || newIndex < 0)) { + return; + } + if (newIndex >= this.props.images.length) { + newIndex = 0; + } + if (newIndex < 0) { + newIndex = this.props.images.length - 1; + } + if (newIndex === this.state.activeIndex) { + return; + } + if (this.props.onChange) { + const activeImage = this.getActiveImage(newIndex); + this.props.onChange(activeImage, newIndex); + } + this.loadImg(newIndex, true); + } + + handleChangeImgState = (width, height, top, left) => { + this.setState({ + width: width, + height: height, + top: top, + left: left, + }); + } + + handleDefaultAction = (type: ActionType) => { + switch (type) { + case ActionType.prev: + this.handleChangeImg(this.state.activeIndex - 1); + break; + case ActionType.next: + this.handleChangeImg(this.state.activeIndex + 1); + break; + case ActionType.zoomIn: + let imgCenterXY = this.getImageCenterXY(); + this.handleZoom(imgCenterXY.x, imgCenterXY.y, 1, this.props.zoomSpeed); + break; + case ActionType.zoomOut: + let imgCenterXY2 = this.getImageCenterXY(); + this.handleZoom(imgCenterXY2.x, imgCenterXY2.y, -1, this.props.zoomSpeed); + break; + case ActionType.rotateLeft: + this.handleRotate(); + break; + case ActionType.rotateRight: + this.handleRotate(true); + break; + case ActionType.reset: + this.loadImg(this.state.activeIndex); + break; + case ActionType.scaleX: + this.handleScaleX(-1); + break; + case ActionType.scaleY: + this.handleScaleY(-1); + break; + case ActionType.download: + this.handleDownload(); + break; + default: + break; + } + } + + handleAction = (config: ToolbarConfig) => { + this.handleDefaultAction(config.actionType); + + if (config.onClick) { + const activeImage = this.getActiveImage(); + config.onClick(activeImage); + } + } + + handleDownload = () => { + const activeImage = this.getActiveImage(); + if (activeImage.downloadUrl) { + location.href = activeImage.downloadUrl; + } + } + + handleScaleX = (newScale: 1 | -1) => { + this.setState({ + scaleX: this.state.scaleX * newScale, + }); + } + + handleScaleY = (newScale: 1 | -1) => { + this.setState({ + scaleY: this.state.scaleY * newScale, + }); + } + + handleScrollZoom = (targetX, targetY, direct) => { + this.handleZoom(targetX, targetY, direct, this.props.zoomSpeed); + } + + handleZoom = (targetX, targetY, direct, scale) => { + let imgCenterXY = this.getImageCenterXY(); + let diffX = targetX - imgCenterXY.x; + let diffY = targetY - imgCenterXY.y; + let top = 0; + let left = 0; + let width = 0; + let height = 0; + let scaleX = 0; + let scaleY = 0; + if (this.state.width === 0) { + const [imgWidth, imgHeight] = this.getImgWidthHeight( + this.state.imageWidth, + this.state.imageHeight, + ); + left = (this.containerWidth - imgWidth) / 2; + top = (this.containerHeight - this.footerHeight - imgHeight) / 2; + width = this.state.width + imgWidth; + height = this.state.height + imgHeight; + scaleX = scaleY = 1; + } else { + let directX = this.state.scaleX > 0 ? 1 : -1; + let directY = this.state.scaleY > 0 ? 1 : -1; + scaleX = this.state.scaleX + scale * direct * directX; + scaleY = this.state.scaleY + scale * direct * directY; + if (Math.abs(scaleX) < 0.1 || Math.abs(scaleY) < 0.1) { + return; + } + top = this.state.top + -direct * diffY / this.state.scaleX * scale * directX; + left = this.state.left + -direct * diffX / this.state.scaleY * scale * directY; + width = this.state.width; + height = this.state.height; + } + this.setState({ + width: width, + scaleX: scaleX, + scaleY: scaleY, + height: height, + top: top, + left: left, + loading: false, + }); + } + + getImageCenterXY = () => { + return { + x: this.state.left + this.state.width / 2, + y: this.state.top + this.state.height / 2, + }; + } + + handleRotate = (isRight: boolean = false) => { + this.setState({ + rotate: this.state.rotate + 90 * (isRight ? 1 : -1), + }); + } + + handleResize = () => { + this.setContainerWidthHeight(); + if (this.props.visible) { + let left = (this.containerWidth - this.state.width) / 2; + let top = (this.containerHeight - this.state.height - this.footerHeight) / 2; + this.setState({ + left: left, + top: top, + }); + } + } + + handleKeydown = (e) => { + let keyCode = e.keyCode || e.which || e.charCode; + let isFeatrue = false; + switch (keyCode) { + // key: esc + case 27: + this.props.onClose(); + isFeatrue = true; + break; + // key: ← + case 37: + if (e.ctrlKey) { + this.handleDefaultAction(ActionType.rotateLeft); + } else { + this.handleDefaultAction(ActionType.prev); + } + isFeatrue = true; + break; + // key: → + case 39: + if (e.ctrlKey) { + this.handleDefaultAction(ActionType.rotateRight); + } else { + this.handleDefaultAction(ActionType.next); + } + isFeatrue = true; + break; + // key: ↑ + case 38: + this.handleDefaultAction(ActionType.zoomIn); + isFeatrue = true; + break; + // key: ↓ + case 40: + this.handleDefaultAction(ActionType.zoomOut); + isFeatrue = true; + break; + // key: Ctrl + 1 + case 49: + if (e.ctrlKey) { + this.loadImg(this.state.activeIndex); + isFeatrue = true; + } + break; + default: + break; + } + if (isFeatrue) { + e.preventDefault(); + } + } + + handleTransitionEnd = () => { + if (!this.state.transitionEnd || this.state.visibleStart) { + this.setState({ + visibleStart: false, + transitionEnd: true, + }); + } + } + + bindEvent(remove: boolean = false) { + let funcName = 'addEventListener'; + if (remove) { + funcName = 'removeEventListener'; + } + if (!this.props.disableKeyboardSupport) { + document[funcName]('keydown', this.handleKeydown, false); + } + } + + componentWillUnmount() { + this.bindEvent(true); + (this.refs['viewerCore'] as HTMLDivElement).removeEventListener( + 'transitionend', + this.handleTransitionEnd, + false, + ); + } + + componentWillReceiveProps(nextProps: ViewerProps) { + if (!this.props.visible && nextProps.visible) { + this.startVisible(nextProps.activeIndex); + return; + } + if (this.props.visible && !nextProps.visible) { + this.bindEvent(true); + this.handleZoom( + this.containerWidth / 2, + (this.containerHeight - this.footerHeight) / 2, + -1, + (this.state.scaleX > 0 ? 1 : -1) * this.state.scaleX - 0.11, + ); + setTimeout(() => { + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + this.setState({ + visible: false, + transitionEnd: false, + width: 0, + height: 0, + scaleX: this.props.defaultScale, + scaleY: this.props.defaultScale, + rotate: 1, + imageWidth: 0, + imageHeight: 0, + loadFailed: false, + }); + }, transitionDuration); + return; + } + if (this.props.activeIndex !== nextProps.activeIndex) { + this.handleChangeImg(nextProps.activeIndex); + return; + } + } + + handleCanvasMouseDown = e => { + this.props.onMaskClick(e); + } + + getActiveImage = (activeIndex = undefined) => { + let activeImg: ImageDecorator = { + src: '', + alt: '', + downloadUrl: '', + }; + + let images = this.props.images || []; + let realActiveIndex = null; + if (activeIndex !== undefined) { + realActiveIndex = activeIndex; + } else { + realActiveIndex = this.state.activeIndex; + } + if (images.length > 0 && realActiveIndex >= 0) { + activeImg = images[realActiveIndex]; + } + + return activeImg; + } + + handleMouseScroll = (e) => { + if (this.props.disableMouseZoom) { + return; + } + e.preventDefault(); + let direct: 0 | 1 | -1 = 0; + const value = e.deltaY; + if (value === 0) { + direct = 0; + } else { + direct = value > 0 ? -1 : 1; + } + if (direct !== 0) { + let x = e.clientX; + let y = e.clientY; + if (this.props.container) { + const containerRect = this.props.container.getBoundingClientRect(); + x -= containerRect.left; + y -= containerRect.top; + } + this.handleScrollZoom(x, y, direct); + } + } + + render() { + let activeImg: ImageDecorator = { + src: '', + alt: '', + }; + + let zIndex = 1000; + + if (this.props.zIndex) { + zIndex = this.props.zIndex; + } + + let viewerStryle: React.CSSProperties = { + opacity: this.state.visible ? 1 : 0, + }; + + if (!this.state.visible && this.state.transitionEnd) { + viewerStryle.display = 'none'; + } + if (!this.state.visible && this.state.visibleStart) { + viewerStryle.display = 'block'; + } + if (this.state.visible && this.state.transitionEnd) { + activeImg = this.getActiveImage(); + } + + let className = `${this.prefixCls} ${this.prefixCls}-transition`; + if (this.props.container) { + className += ` ${this.prefixCls}-inline`; + } + + return ( +
+
+ {this.props.noClose || ( +
+ +
+ )} + + {this.props.noFooter || ( +
+ {this.props.noToolbar || ( + + )} + {this.props.noNavbar || ( + + )} +
+ )} +
+ ); + } +} diff --git a/src/ViewerCore.tsx b/src/ViewerCore.tsx index 6b86512..08fd168 100644 --- a/src/ViewerCore.tsx +++ b/src/ViewerCore.tsx @@ -9,7 +9,21 @@ import * as constants from './constants'; function noop() { } -const transitionDuration = 300; +// const transitionDuration = 300; + +const ACTION_TYPES = { + setVisible: 'setVisible', + setActiveIndex: 'setActiveIndex', + update: 'update', + clear: 'clear', +}; + +function createAction(type, payload) { + return { + type, + payload: payload || {}, + }; +} export interface ViewerCoreState { visible?: boolean; @@ -27,432 +41,471 @@ export interface ViewerCoreState { scaleY?: number; loading?: boolean; loadFailed?: boolean; + startLoading: boolean; } -export default class ViewerCore extends React.Component { - static defaultProps: Partial = { +export default React.forwardRef((props: ViewerProps, ref) => { + const { + visible = false, + onClose = noop, + images = [], + activeIndex = 0, + zIndex = 1000, + drag = true, + attribute = true, + zoomable = true, + rotatable = true, + scalable = true, + onMaskClick = noop, + changeable = true, + customToolbar = (toolbars) => toolbars, + zoomSpeed = .05, + disableKeyboardSupport = false, + noResetZoomAfterChange = false, + noLimitInitializationSize = false, + defaultScale = 1, + loop = true, + disableMouseZoom = false, + downloadable = false, + noImgDetails = false, + noToolbar = false, + } = props; + + const initialState: ViewerCoreState = { visible: false, - onClose: noop, - images: [], - activeIndex: 0, - zIndex: 1000, - drag: true, - attribute: true, - zoomable: true, - rotatable: true, - scalable: true, - onMaskClick: noop, - changeable: true, - customToolbar: (toolbars) => toolbars, - zoomSpeed: .05, - disableKeyboardSupport: false, - noResetZoomAfterChange: false, - noLimitInitializationSize: false, - defaultScale: 1, - loop: true, - disableMouseZoom: false, + visibleStart: false, + transitionEnd: false, + activeIndex: props.activeIndex, + width: 0, + height: 0, + top: 15, + left: null, + rotate: 0, + imageWidth: 0, + imageHeight: 0, + scaleX: defaultScale, + scaleY: defaultScale, + loading: false, + loadFailed: false, + startLoading: false, }; - - private prefixCls: string; - private containerWidth: number; - private containerHeight: number; - private footerHeight: number; - - constructor(props) { - super(props); - - this.prefixCls = 'react-viewer'; - - this.state = { - visible: false, - visibleStart: false, - transitionEnd: false, - activeIndex: this.props.activeIndex, - width: 0, - height: 0, - top: 15, - left: null, - rotate: 0, - imageWidth: 0, - imageHeight: 0, - scaleX: this.props.defaultScale, - scaleY: this.props.defaultScale, - loading: false, - loadFailed: false, + function setContainerWidthHeight() { + let width = window.innerWidth; + let height = window.innerHeight; + if (props.container) { + width = props.container.offsetWidth; + height = props.container.offsetHeight; + } + return { + width, + height, }; - - this.setContainerWidthHeight(); - this.footerHeight = constants.FOOTER_HEIGHT; } - - setContainerWidthHeight() { - this.containerWidth = window.innerWidth; - this.containerHeight = window.innerHeight; - if (this.props.container) { - this.containerWidth = this.props.container.offsetWidth; - this.containerHeight = this.props.container.offsetHeight; - this.setInlineContainerHeight(); + const containerSize = React.useRef(setContainerWidthHeight()); + const footerHeight = constants.FOOTER_HEIGHT; + function reducer(s: ViewerCoreState, action): typeof initialState { + switch (action.type) { + case ACTION_TYPES.setVisible: + return { + ...s, + visible: action.payload.visible, + }; + case ACTION_TYPES.setActiveIndex: + return { + ...s, + activeIndex: action.payload.index, + }; + case ACTION_TYPES.update: + return { + ...s, + ...action.payload, + }; + case ACTION_TYPES.clear: + return { + ...s, + width: 0, + height: 0, + scaleX: defaultScale, + scaleY: defaultScale, + rotate: 1, + imageWidth: 0, + imageHeight: 0, + loadFailed: false, + top: 0, + left: 0, + loading: false, + }; + default: + break; } + return s; } - setInlineContainerHeight() { - const core = (this.refs['viewerCore'] as HTMLDivElement); - if (core) { - this.containerHeight = core.offsetHeight; - } - } + const viewerCore = React.useRef(null); + const init = React.useRef(false); + const currentLoadIndex = React.useRef(0); + const [ state, dispatch ] = React.useReducer<(s: any, a: any) => ViewerCoreState>(reducer, initialState); - handleClose = () => { - this.props.onClose(); - } + React.useEffect(() => { + init.current = true; - startVisible(activeIndex: number) { - if (!this.props.container) { - document.body.style.overflow = 'hidden'; - if (document.body.scrollHeight > document.body.clientHeight) { - document.body.style.paddingRight = '15px'; - } - } - this.setState({ - visibleStart: true, - }); - setTimeout(() => { - this.setState({ - visible: true, - activeIndex, - }); + return () => { + init.current = false; + }; + }, []); + + React.useEffect(() => { + containerSize.current = setContainerWidthHeight(); + }, [props.container]); + + React.useEffect(() => { + if (props.visible) { setTimeout(() => { - this.bindEvent(); - this.loadImg(activeIndex); - }, 300); - }, 10); - } - - componentDidMount() { - const core = (this.refs['viewerCore'] as HTMLDivElement); - core.addEventListener( - 'transitionend', - this.handleTransitionEnd, - false, - ); - // Though onWheel can be directly used on the div "viewerCore", to be able to - // prevent default action, a listener is added here instead - (this.refs['viewerCore'] as HTMLDivElement).addEventListener( - 'wheel', - this.handleMouseScroll, - false, - ); - if (this.containerHeight === 0) { - this.setInlineContainerHeight(); + if (init.current) { + dispatch(createAction(ACTION_TYPES.setVisible, { + visible: true, + })); + } + }, 10); + } + }, [props.visible]); + + React.useEffect(() => { + bindEvent(); + + return () => { + bindEvent(true); + }; + }); + + React.useEffect(() => { + if (visible) { + if (!props.container) { + document.body.style.overflow = 'hidden'; + if (document.body.scrollHeight > document.body.clientHeight) { + document.body.style.paddingRight = '15px'; + } + } + } else { + dispatch(createAction(ACTION_TYPES.clear, {})); + } + + return () => { + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + }; + }, [state.visible]); + + React.useEffect(() => { + dispatch(createAction(ACTION_TYPES.setActiveIndex, { + index: activeIndex, + })); + }, [activeIndex]); + + function loadImg(currentActiveIndex, isReset = false) { + dispatch(createAction(ACTION_TYPES.update, { + loading: true, + loadFailed: false, + })); + let activeImage: ImageDecorator = null; + if (images.length > 0) { + activeImage = images[currentActiveIndex]; + } + let loadComplete = false; + let img = new Image(); + img.onload = () => { + if (!init.current) { + return; + } + if (!loadComplete) { + loadImgSuccess(img.width, img.height); + } + }; + img.onerror = () => { + if (!init.current) { + return; + } + if (props.defaultImg) { + dispatch(createAction(ACTION_TYPES.update, { + loading: false, + loadFailed: true, + startLoading: false, + })); + } else { + dispatch(createAction(ACTION_TYPES.update, { + loading: false, + loadFailed: false, + startLoading: false, + })); + } + }; + img.src = activeImage.src; + if (img.complete) { + loadComplete = true; + loadImgSuccess(img.width, img.height); + } + function loadImgSuccess(imgWidth, imgHeight, isNewImage: boolean = false) { + if (currentActiveIndex !== currentLoadIndex.current) { + return; + } + let realImgWidth = imgWidth; + let realImgHeight = imgHeight; + if (props.defaultSize) { + realImgWidth = props.defaultSize.width; + realImgHeight = props.defaultSize.height; + } + if (activeImage.defaultSize) { + realImgWidth = activeImage.defaultSize.width; + realImgHeight = activeImage.defaultSize.height; + } + let [width, height] = getImgWidthHeight(realImgWidth, realImgHeight); + let left = (containerSize.current.width - width) / 2; + let top = (containerSize.current.height - height - footerHeight) / 2; + let scaleX = defaultScale; + let scaleY = defaultScale; + if (noResetZoomAfterChange && !isReset) { + scaleX = state.scaleX; + scaleY = state.scaleY; + } + dispatch(createAction(ACTION_TYPES.update, { + width: width, + height: height, + left: left, + top: top, + imageWidth: imgWidth, + imageHeight: imgHeight, + loading: false, + rotate: 0, + scaleX: scaleX, + scaleY: scaleY, + loadFailed: false, + startLoading: false, + })); } - this.startVisible(this.state.activeIndex); } - getImgWidthHeight(imgWidth, imgHeight) { + React.useEffect(() => { + if (state.startLoading) { + currentLoadIndex.current = state.activeIndex; + loadImg(state.activeIndex); + } + }, [state.startLoading, state.activeIndex]); + + React.useEffect(() => { + if (state.activeIndex === null || !state.visible) { + return; + } + dispatch(createAction(ACTION_TYPES.update, { + startLoading: true, + })); + }, [state.activeIndex, state.visible]); + + function getImgWidthHeight(imgWidth, imgHeight) { let width = 0; let height = 0; - let maxWidth = this.containerWidth * 0.8; - let maxHeight = (this.containerHeight - this.footerHeight) * 0.8; + let maxWidth = containerSize.current.width * 0.8; + let maxHeight = (containerSize.current.height - footerHeight) * 0.8; width = Math.min(maxWidth, imgWidth); height = width / imgWidth * imgHeight; if (height > maxHeight) { height = maxHeight; width = height / imgHeight * imgWidth; } - if (this.props.noLimitInitializationSize) { + if (noLimitInitializationSize) { width = imgWidth; height = imgHeight; } return [width, height]; } - loadImgSuccess = (activeImage: ImageDecorator, imgWidth, imgHeight, isNewImage: boolean) => { - let realImgWidth = imgWidth; - let realImgHeight = imgHeight; - if (this.props.defaultSize) { - realImgWidth = this.props.defaultSize.width; - realImgHeight = this.props.defaultSize.height; - } - if (activeImage.defaultSize) { - realImgWidth = activeImage.defaultSize.width; - realImgHeight = activeImage.defaultSize.height; - } - let [width, height] = this.getImgWidthHeight(realImgWidth, realImgHeight); - let left = (this.containerWidth - width) / 2; - let top = (this.containerHeight - height - this.footerHeight) / 2; - let scaleX = this.props.defaultScale; - let scaleY = this.props.defaultScale; - if (this.props.noResetZoomAfterChange && isNewImage) { - scaleX = this.state.scaleX; - scaleY = this.state.scaleY; - } - this.setState({ - width: width, - height: height, - left: left, - top: top, - imageWidth: imgWidth, - imageHeight: imgHeight, - loading: false, - rotate: 0, - scaleX: scaleX, - scaleY: scaleY, - }); - } - - loadImg(activeIndex, isNewImage: boolean = false) { - let activeImage: ImageDecorator = null; - let images = this.props.images || []; - if (images.length > 0) { - activeImage = images[activeIndex]; - } - let loadComplete = false; - let img = new Image(); - this.setState({ - activeIndex: activeIndex, - loading: true, - loadFailed: false, - }, () => { - img.onload = () => { - if (!loadComplete) { - this.loadImgSuccess(activeImage, img.width, img.height, isNewImage); - } - }; - img.onerror = () => { - if (this.props.defaultImg) { - this.setState({ - loadFailed: true, - }); - const deafultImgWidth = this.props.defaultImg.width || this.containerWidth * .5; - const defaultImgHeight = this.props.defaultImg.height || this.containerHeight * .5; - this.loadImgSuccess(activeImage, deafultImgWidth, defaultImgHeight, isNewImage); - } else { - this.setState({ - activeIndex: activeIndex, - imageWidth: 0, - imageHeight: 0, - loading: false, - }); - } - }; - img.src = activeImage.src; - if (img.complete) { - loadComplete = true; - this.loadImgSuccess(activeImage, img.width, img.height, isNewImage); - } - }); - } - - handleChangeImg = (newIndex: number) => { - if (!this.props.loop && (newIndex >= this.props.images.length || newIndex < 0)) { + function handleChangeImg(newIndex: number) { + if (!loop && (newIndex >= images.length || newIndex < 0)) { return; } - if (newIndex >= this.props.images.length) { + if (newIndex >= images.length) { newIndex = 0; } if (newIndex < 0) { - newIndex = this.props.images.length - 1; + newIndex = images.length - 1; } - if (newIndex === this.state.activeIndex) { + if (newIndex === state.activeIndex) { return; } - if (this.props.onChange) { - const activeImage = this.getActiveImage(newIndex); - this.props.onChange(activeImage, newIndex); + if (props.onChange) { + const activeImage = getActiveImage(newIndex); + props.onChange(activeImage, newIndex); } - this.loadImg(newIndex, true); + dispatch(createAction(ACTION_TYPES.setActiveIndex, { + index: newIndex, + })); } - handleChangeImgState = (width, height, top, left) => { - this.setState({ - width: width, - height: height, - top: top, - left: left, - }); + function getActiveImage(activeIndex2 = undefined) { + let activeImg2: ImageDecorator = { + src: '', + alt: '', + downloadUrl: '', + }; + + let realActiveIndex = null; + if (activeIndex2 !== undefined) { + realActiveIndex = activeIndex2; + } else { + realActiveIndex = state.activeIndex; + } + if (images.length > 0 && realActiveIndex >= 0) { + activeImg2 = images[realActiveIndex]; + } + + return activeImg2; } - handleDefaultAction = (type: ActionType) => { + function handleDownload() { + const activeImage = getActiveImage(); + if (activeImage.downloadUrl) { + location.href = activeImage.downloadUrl; + } + } + + function handleScaleX(newScale: 1 | -1) { + dispatch(createAction(ACTION_TYPES.update, { + scaleX: state.scaleX * newScale, + })); + } + + function handleScaleY(newScale: 1 | -1) { + dispatch(createAction(ACTION_TYPES.update, { + scaleY: state.scaleY * newScale, + })); + } + + function handleRotate(isRight: boolean = false) { + dispatch(createAction(ACTION_TYPES.update, { + rotate: state.rotate + 90 * (isRight ? 1 : -1), + })); + } + + function handleDefaultAction(type: ActionType) { switch (type) { case ActionType.prev: - this.handleChangeImg(this.state.activeIndex - 1); + handleChangeImg(state.activeIndex - 1); break; case ActionType.next: - this.handleChangeImg(this.state.activeIndex + 1); + handleChangeImg(state.activeIndex + 1); break; case ActionType.zoomIn: - let imgCenterXY = this.getImageCenterXY(); - this.handleZoom(imgCenterXY.x, imgCenterXY.y, 1, this.props.zoomSpeed); + let imgCenterXY = getImageCenterXY(); + handleZoom(imgCenterXY.x, imgCenterXY.y, 1, zoomSpeed); break; case ActionType.zoomOut: - let imgCenterXY2 = this.getImageCenterXY(); - this.handleZoom(imgCenterXY2.x, imgCenterXY2.y, -1, this.props.zoomSpeed); + let imgCenterXY2 = getImageCenterXY(); + handleZoom(imgCenterXY2.x, imgCenterXY2.y, -1, zoomSpeed); break; case ActionType.rotateLeft: - this.handleRotate(); + handleRotate(); break; case ActionType.rotateRight: - this.handleRotate(true); + handleRotate(true); break; case ActionType.reset: - this.loadImg(this.state.activeIndex); + loadImg(state.activeIndex, true); break; case ActionType.scaleX: - this.handleScaleX(-1); + handleScaleX(-1); break; case ActionType.scaleY: - this.handleScaleY(-1); + handleScaleY(-1); break; case ActionType.download: - this.handleDownload(); + handleDownload(); break; default: break; } } - handleAction = (config: ToolbarConfig) => { - this.handleDefaultAction(config.actionType); + function handleAction(config: ToolbarConfig) { + handleDefaultAction(config.actionType); if (config.onClick) { - const activeImage = this.getActiveImage(); + const activeImage = getActiveImage(); config.onClick(activeImage); } } - handleDownload = () => { - const activeImage = this.getActiveImage(); - if (activeImage.downloadUrl) { - location.href = activeImage.downloadUrl; - } - } - - handleScaleX = (newScale: 1 | -1) => { - this.setState({ - scaleX: this.state.scaleX * newScale, - }); - } - - handleScaleY = (newScale: 1 | -1) => { - this.setState({ - scaleY: this.state.scaleY * newScale, - }); - } - - handleScrollZoom = (targetX, targetY, direct) => { - this.handleZoom(targetX, targetY, direct, this.props.zoomSpeed); - } - - handleZoom = (targetX, targetY, direct, scale) => { - let imgCenterXY = this.getImageCenterXY(); - let diffX = targetX - imgCenterXY.x; - let diffY = targetY - imgCenterXY.y; - let top = 0; - let left = 0; - let width = 0; - let height = 0; - let scaleX = 0; - let scaleY = 0; - if (this.state.width === 0) { - const [imgWidth, imgHeight] = this.getImgWidthHeight( - this.state.imageWidth, - this.state.imageHeight, - ); - left = (this.containerWidth - imgWidth) / 2; - top = (this.containerHeight - this.footerHeight - imgHeight) / 2; - width = this.state.width + imgWidth; - height = this.state.height + imgHeight; - scaleX = scaleY = 1; - } else { - let directX = this.state.scaleX > 0 ? 1 : -1; - let directY = this.state.scaleY > 0 ? 1 : -1; - scaleX = this.state.scaleX + scale * direct * directX; - scaleY = this.state.scaleY + scale * direct * directY; - if (Math.abs(scaleX) < 0.1 || Math.abs(scaleY) < 0.1) { - return; - } - top = this.state.top + -direct * diffY / this.state.scaleX * scale * directX; - left = this.state.left + -direct * diffX / this.state.scaleY * scale * directY; - width = this.state.width; - height = this.state.height; - } - this.setState({ + function handleChangeImgState(width, height, top, left) { + dispatch(createAction(ACTION_TYPES.update, { width: width, - scaleX: scaleX, - scaleY: scaleY, height: height, top: top, left: left, - loading: false, - }); + })); } - getImageCenterXY = () => { - return { - x: this.state.left + this.state.width / 2, - y: this.state.top + this.state.height / 2, - }; + function handleResize() {} + + function handleCanvasMouseDown(e) { + onMaskClick(e); } - handleRotate = (isRight: boolean = false) => { - this.setState({ - rotate: this.state.rotate + 90 * (isRight ? 1 : -1), - }); - } - - handleResize = () => { - this.setContainerWidthHeight(); - if (this.props.visible) { - let left = (this.containerWidth - this.state.width) / 2; - let top = (this.containerHeight - this.state.height - this.footerHeight) / 2; - this.setState({ - left: left, - top: top, - }); + function bindEvent(remove: boolean = false) { + let funcName = 'addEventListener'; + if (remove) { + funcName = 'removeEventListener'; + } + if (!disableKeyboardSupport) { + document[funcName]('keydown', handleKeydown, false); + } + if (viewerCore.current) { + viewerCore.current[funcName]( + 'wheel', + handleMouseScroll, + false, + ); } } - handleKeydown = (e) => { + function handleKeydown(e) { let keyCode = e.keyCode || e.which || e.charCode; let isFeatrue = false; switch (keyCode) { // key: esc case 27: - this.props.onClose(); + onClose(); isFeatrue = true; break; // key: ← case 37: if (e.ctrlKey) { - this.handleDefaultAction(ActionType.rotateLeft); + handleDefaultAction(ActionType.rotateLeft); } else { - this.handleDefaultAction(ActionType.prev); + handleDefaultAction(ActionType.prev); } isFeatrue = true; break; // key: → case 39: if (e.ctrlKey) { - this.handleDefaultAction(ActionType.rotateRight); + handleDefaultAction(ActionType.rotateRight); } else { - this.handleDefaultAction(ActionType.next); + handleDefaultAction(ActionType.next); } isFeatrue = true; break; // key: ↑ case 38: - this.handleDefaultAction(ActionType.zoomIn); + handleDefaultAction(ActionType.zoomIn); isFeatrue = true; break; // key: ↓ case 40: - this.handleDefaultAction(ActionType.zoomOut); + handleDefaultAction(ActionType.zoomOut); isFeatrue = true; break; // key: Ctrl + 1 case 49: if (e.ctrlKey) { - this.loadImg(this.state.activeIndex); + loadImg(state.activeIndex); isFeatrue = true; } break; @@ -464,98 +517,11 @@ export default class ViewerCore extends React.Component { - if (!this.state.transitionEnd || this.state.visibleStart) { - this.setState({ - visibleStart: false, - transitionEnd: true, - }); - } - } - - bindEvent(remove: boolean = false) { - let funcName = 'addEventListener'; - if (remove) { - funcName = 'removeEventListener'; - } - if (!this.props.disableKeyboardSupport) { - document[funcName]('keydown', this.handleKeydown, false); - } - } - - componentWillUnmount() { - this.bindEvent(true); - (this.refs['viewerCore'] as HTMLDivElement).removeEventListener( - 'transitionend', - this.handleTransitionEnd, - false, - ); - } - - componentWillReceiveProps(nextProps: ViewerProps) { - if (!this.props.visible && nextProps.visible) { - this.startVisible(nextProps.activeIndex); + function handleMouseScroll(e) { + if (disableMouseZoom) { return; } - if (this.props.visible && !nextProps.visible) { - this.bindEvent(true); - this.handleZoom( - this.containerWidth / 2, - (this.containerHeight - this.footerHeight) / 2, - -1, - (this.state.scaleX > 0 ? 1 : -1) * this.state.scaleX - 0.11, - ); - setTimeout(() => { - document.body.style.overflow = ''; - document.body.style.paddingRight = ''; - this.setState({ - visible: false, - transitionEnd: false, - width: 0, - height: 0, - scaleX: this.props.defaultScale, - scaleY: this.props.defaultScale, - rotate: 1, - imageWidth: 0, - imageHeight: 0, - loadFailed: false, - }); - }, transitionDuration); - return; - } - if (this.props.activeIndex !== nextProps.activeIndex) { - this.handleChangeImg(nextProps.activeIndex); - return; - } - } - - handleCanvasMouseDown = e => { - this.props.onMaskClick(e); - } - - getActiveImage = (activeIndex = undefined) => { - let activeImg: ImageDecorator = { - src: '', - alt: '', - downloadUrl: '', - }; - - let images = this.props.images || []; - let realActiveIndex = null; - if (activeIndex !== undefined) { - realActiveIndex = activeIndex; - } else { - realActiveIndex = this.state.activeIndex; - } - if (images.length > 0 && realActiveIndex >= 0) { - activeImg = images[realActiveIndex]; - } - - return activeImg; - } - - handleMouseScroll = (e) => { - if (this.props.disableMouseZoom) { + if (state.loading) { return; } e.preventDefault(); @@ -569,111 +535,160 @@ export default class ViewerCore extends React.Component -
- {this.props.noClose || ( -
- -
- )} - - {this.props.noFooter || ( -
- {this.props.noToolbar || ( - - )} - {this.props.noNavbar || ( - - )} -
- )} -
- ); } -} + + function handleZoom(targetX, targetY, direct, scale) { + let imgCenterXY = getImageCenterXY(); + let diffX = targetX - imgCenterXY.x; + let diffY = targetY - imgCenterXY.y; + let top = 0; + let left = 0; + let width = 0; + let height = 0; + let scaleX = 0; + let scaleY = 0; + if (state.width === 0) { + const [imgWidth, imgHeight] = getImgWidthHeight( + state.imageWidth, + state.imageHeight, + ); + left = (containerSize.current.width - imgWidth) / 2; + top = (containerSize.current.height - footerHeight - imgHeight) / 2; + width = state.width + imgWidth; + height = state.height + imgHeight; + scaleX = scaleY = 1; + } else { + let directX = state.scaleX > 0 ? 1 : -1; + let directY = state.scaleY > 0 ? 1 : -1; + scaleX = state.scaleX + scale * direct * directX; + scaleY = state.scaleY + scale * direct * directY; + if (Math.abs(scaleX) < 0.1 || Math.abs(scaleY) < 0.1) { + return; + } + top = state.top + -direct * diffY / state.scaleX * scale * directX; + left = state.left + -direct * diffX / state.scaleY * scale * directY; + width = state.width; + height = state.height; + } + dispatch(createAction(ACTION_TYPES.update, { + width: width, + scaleX: scaleX, + scaleY: scaleY, + height: height, + top: top, + left: left, + loading: false, + })); + } + + const prefixCls = 'react-viewer'; + + let className = `${prefixCls} ${prefixCls}-transition`; + if (props.container) { + className += ` ${prefixCls}-inline`; + } + + let viewerStryle: React.CSSProperties = { + opacity: (props.visible && state.visible) ? 1 : 0, + display: (props.visible || state.visible) ? 'block' : 'none', + }; + + let activeImg: ImageDecorator = { + src: '', + alt: '', + }; + + if (props.visible && visible && !state.loading && state.activeIndex !== null && !state.startLoading) { + activeImg = getActiveImage(); + } + + return ( +
{ + if (!props.visible) { + dispatch(createAction(ACTION_TYPES.setVisible, { + visible: false, + })); + } + }} + ref={viewerCore} + > +
+ {props.noClose || ( +
{ + onClose(); + }} + style={{ zIndex: zIndex + 10 }} + > + +
+ )} + + {props.noFooter || ( +
+ {noToolbar || ( + + )} + {props.noNavbar || ( + + )} +
+ )} +
+ ); +}); diff --git a/src/ViewerNav.tsx b/src/ViewerNav.tsx index 03b5e6c..e485929 100644 --- a/src/ViewerNav.tsx +++ b/src/ViewerNav.tsx @@ -8,39 +8,35 @@ export interface ViewerNavProps { onChangeImg: (index: number) => void; } -export default class ViewerNav extends React.Component { - static defaultProps = { - activeIndex: 0, - }; +export default function ViewerNav(props: ViewerNavProps) { + const { activeIndex = 0 } = props; - handleChangeImg = (newIndex) => { - if (this.props.activeIndex === newIndex) { + function handleChangeImg(newIndex) { + if (activeIndex === newIndex) { return; } - this.props.onChangeImg(newIndex); + props.onChangeImg(newIndex); } - render() { - let marginLeft = `calc(50% - ${this.props.activeIndex + 1} * 31px)`; - let listStyle = { - marginLeft: marginLeft, - }; + let marginLeft = `calc(50% - ${activeIndex + 1} * 31px)`; + let listStyle = { + marginLeft: marginLeft, + }; - return ( -
-
    - {this.props.images.map((item, index) => -
  • { this.handleChangeImg(index); }} - > - {item.alt} -
  • , - ) - } -
-
- ); - } + return ( +
+
    + {props.images.map((item, index) => +
  • { handleChangeImg(index); }} + > + {item.alt} +
  • , + ) + } +
+
+ ); } diff --git a/src/ViewerToolbar.tsx b/src/ViewerToolbar.tsx index a58b95f..dd5eb5f 100644 --- a/src/ViewerToolbar.tsx +++ b/src/ViewerToolbar.tsx @@ -67,17 +67,12 @@ function deleteToolbarFromKey(toolbars: ToolbarConfig[], keys: string[]) { return targetToolbar; } -export default class ViewerToolbar extends React.Component { - - constructor() { - super(); +export default function ViewerToolbar(props: ViewerToolbarProps) { + function handleAction(config: ToolbarConfig) { + props.onAction(config); } - handleAction(config: ToolbarConfig) { - this.props.onAction(config); - } - - renderAction = (config: ToolbarConfig) => { + function renderAction(config: ToolbarConfig) { let content = null; // default toolbar if (typeof ActionType[config.actionType] !== 'undefined') { @@ -90,49 +85,46 @@ export default class ViewerToolbar extends React.Component {this.handleAction(config); }} + className={`${props.prefixCls}-btn`} + onClick={() => {handleAction(config); }} data-key={config.key} > {content} ); } - - render() { - let attributeNode = this.props.attribute ? ( -

- {this.props.alt && `${this.props.alt}`} - {this.props.noImgDetails || - {`(${this.props.width} x ${this.props.height})`} - } -

- ) : null; - let toolbars = this.props.toolbars; - if (!this.props.zoomable) { - toolbars = deleteToolbarFromKey(toolbars, ['zoomIn', 'zoomOut']); - } - if (!this.props.changeable) { - toolbars = deleteToolbarFromKey(toolbars, ['prev', 'next']); - } - if (!this.props.rotatable) { - toolbars = deleteToolbarFromKey(toolbars, ['rotateLeft', 'rotateRight']); - } - if (!this.props.scalable) { - toolbars = deleteToolbarFromKey(toolbars, ['scaleX', 'scaleY']); - } - if (!this.props.downloadable) { - toolbars = deleteToolbarFromKey(toolbars, ['download']); - } - return ( -
- {attributeNode} -
    - {toolbars.map(item => { - return this.renderAction(item); - })} -
-
- ); + let attributeNode = props.attribute ? ( +

+ {props.alt && `${props.alt}`} + {props.noImgDetails || + {`(${props.width} x ${props.height})`} + } +

+ ) : null; + let toolbars = props.toolbars; + if (!props.zoomable) { + toolbars = deleteToolbarFromKey(toolbars, ['zoomIn', 'zoomOut']); } + if (!props.changeable) { + toolbars = deleteToolbarFromKey(toolbars, ['prev', 'next']); + } + if (!props.rotatable) { + toolbars = deleteToolbarFromKey(toolbars, ['rotateLeft', 'rotateRight']); + } + if (!props.scalable) { + toolbars = deleteToolbarFromKey(toolbars, ['scaleX', 'scaleY']); + } + if (!props.downloadable) { + toolbars = deleteToolbarFromKey(toolbars, ['download']); + } + return ( +
+ {attributeNode} +
    + {toolbars.map(item => { + return renderAction(item); + })} +
+
+ ); }