UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

460 lines 15.9 kB
import _isFunction from "lodash/isFunction"; import _isEqual from "lodash/isEqual"; import React from "react"; import BaseComponent from "../_base/baseComponent"; import PropTypes from "prop-types"; import { cssClasses, numbers } from '@douyinfe/semi-foundation/lib/es/image/constants'; import cls from "classnames"; import Portal from "../_portal"; import { IconArrowLeft, IconArrowRight } from "@douyinfe/semi-icons"; import Header from "./previewHeader"; import Footer from "./previewFooter"; import PreviewImage from "./previewImage"; import PreviewInnerFoundation from '@douyinfe/semi-foundation/lib/es/image/previewInnerFoundation'; import { PreviewContext } from "./previewContext"; import { getScrollbarWidth } from "../_utils"; const prefixCls = cssClasses.PREFIX; export default class PreviewInner extends BaseComponent { get adapter() { var _this = this; return Object.assign(Object.assign({}, super.adapter), { getIsInGroup: () => this.isInGroup(), disabledBodyScroll: () => { const { getPopupContainer } = this.props; this.bodyOverflow = document.body.style.overflow || ''; if (!getPopupContainer && this.bodyOverflow !== 'hidden') { document.body.style.overflow = 'hidden'; document.body.style.width = `calc(${this.originBodyWidth || '100%'} - ${this.scrollBarWidth}px)`; } }, enabledBodyScroll: () => { const { getPopupContainer } = this.props; if (!getPopupContainer && this.bodyOverflow !== 'hidden') { document.body.style.overflow = this.bodyOverflow; document.body.style.width = this.originBodyWidth; } }, notifyChange: (index, direction) => { const { onChange, onPrev, onNext } = this.props; _isFunction(onChange) && onChange(index); if (direction === "prev") { onPrev && onPrev(index); } else { onNext && onNext(index); } }, notifyZoom: (zoom, increase) => { const { onZoomIn, onZoomOut } = this.props; if (increase) { _isFunction(onZoomIn) && onZoomIn(zoom); } else { _isFunction(onZoomOut) && onZoomOut(zoom); } }, notifyClose: () => { const { onClose } = this.props; _isFunction(onClose) && onClose(); }, notifyVisibleChange: visible => { const { onVisibleChange } = this.props; _isFunction(onVisibleChange) && onVisibleChange(visible); }, notifyRatioChange: type => { const { onRatioChange } = this.props; _isFunction(onRatioChange) && onRatioChange(type); }, notifyRotateChange: angle => { const { onRotateLeft } = this.props; _isFunction(onRotateLeft) && onRotateLeft(angle); }, notifyDownload: (src, index) => { const { onDownload } = this.props; _isFunction(onDownload) && onDownload(src, index); }, notifyDownloadError: src => { const { onDownloadError } = this.props; _isFunction(onDownloadError) && onDownloadError(src); }, registerKeyDownListener: () => { window && window.addEventListener("keydown", this.handleKeyDown); }, unregisterKeyDownListener: () => { window && window.removeEventListener("keydown", this.handleKeyDown); }, getSetDownloadFunc: () => { var _a, _b; return (_b = (_a = this.context) === null || _a === void 0 ? void 0 : _a.setDownloadName) !== null && _b !== void 0 ? _b : this.props.setDownloadName; }, isValidTarget: e => { const headerDom = this.headerRef && this.headerRef.current; const footerDom = this.footerRef && this.footerRef.current; const leftIconDom = this.leftIconRef && this.leftIconRef.current; const rightIconDom = this.rightIconRef && this.rightIconRef.current; const target = e.target; if (headerDom && headerDom.contains(target) || footerDom && footerDom.contains(target) || leftIconDom && leftIconDom.contains(target) || rightIconDom && rightIconDom.contains(target)) { // Move in the operation area, return false return false; } // Move in the preview area except the operation area, return true return true; }, changeImageZoom: function () { var _a; ((_a = _this.imageRef) === null || _a === void 0 ? void 0 : _a.current) && _this.imageRef.current.foundation.changeZoom(...arguments); } }); } constructor(props) { var _this2; super(props); _this2 = this; this.viewVisibleChange = () => { this.foundation.handleViewVisibleChange(); }; this.handleSwitchImage = direction => { this.foundation.handleSwitchImage(direction); }; this.handleDownload = () => { this.foundation.handleDownload(); }; this.handlePreviewClose = e => { this.foundation.handlePreviewClose(e); }; this.handleAdjustRatio = type => { this.foundation.handleAdjustRatio(type); }; this.handleRotateImage = direction => { this.foundation.handleRotateImage(direction); }; this.handleZoomImage = function (newZoom) { let notify = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; _this2.foundation.handleZoomImage(newZoom, notify); }; this.handleMouseUp = e => { this.foundation.handleMouseUp(e.nativeEvent); }; this.handleMouseMove = e => { this.foundation.handleMouseMove(e); }; this.handleKeyDown = e => { this.foundation.handleKeyDown(e); }; this.onImageError = () => { this.foundation.preloadSingleImage(); }; this.onImageLoad = src => { this.foundation.onImageLoad(src); }; this.handleMouseDown = e => { this.foundation.handleMouseDown(e); }; this.handleWheel = e => { this.foundation.handleWheel(e); }; // 为什么通过 addEventListener 注册 wheel 事件而不是使用 onWheel 事件? // 因为 Passive Event Listeners(https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners) // Passive Event Listeners 是一种优化技术,用于提高滚动性能。在默认情况下,浏览器会假设事件的监听器不会调用 // preventDefault() 方法来阻止事件的默认行为,从而允许进行一些优化操作,例如滚动平滑。 // 对于 Image 而言,如果使用触控板,双指朝不同方向分开放大图片,则需要 preventDefault 防止页面整体放大。 // Why register wheel event through addEventListener instead of using onWheel event? // Because of Passive Event Listeners(an optimization technique used to improve scrolling performance. By default, // the browser will assume that event listeners will not call preventDefault() method to prevent the default behavior of the event, // allowing some optimization operations such as scroll smoothing.) // For Image, if we use the trackpad and spread your fingers in different directions to enlarge the image, we need to preventDefault // to prevent the page from being enlarged as a whole. this.registryImageWrapRef = ref => { if (this.imageWrapRef) { this.imageWrapRef.removeEventListener("wheel", this.handleWheel); } if (ref) { ref.addEventListener("wheel", this.handleWheel, { passive: false }); } this.imageWrapRef = ref; }; this.state = { imgSrc: [], imgLoadStatus: new Map(), zoom: 0.1, currentIndex: 0, ratio: "adaptation", rotation: 0, viewerVisible: true, visible: false, preloadAfterVisibleChange: true, direction: "" }; this.foundation = new PreviewInnerFoundation(this.adapter); this.bodyOverflow = ''; this.originBodyWidth = '100%'; this.scrollBarWidth = 0; this.imageWrapRef = null; this.imageRef = /*#__PURE__*/React.createRef(); this.headerRef = /*#__PURE__*/React.createRef(); this.footerRef = /*#__PURE__*/React.createRef(); this.leftIconRef = /*#__PURE__*/React.createRef(); this.rightIconRef = /*#__PURE__*/React.createRef(); } static getDerivedStateFromProps(props, state) { const willUpdateStates = {}; let src = []; if (props.visible) { // if src in props src = Array.isArray(props.src) ? props.src : [props.src]; } if (!_isEqual(src, state.imgSrc)) { willUpdateStates.imgSrc = src; } if (props.visible !== state.visible) { willUpdateStates.visible = props.visible; if (props.visible) { willUpdateStates.preloadAfterVisibleChange = true; willUpdateStates.viewerVisible = true; willUpdateStates.rotation = 0; willUpdateStates.ratio = 'adaptation'; } } if ("currentIndex" in props && props.currentIndex !== state.currentIndex) { willUpdateStates.currentIndex = props.currentIndex; // ratio will set to adaptation when change picture, // attention: If the ratio is controlled, the ratio should not change as the index changes willUpdateStates.ratio = 'adaptation'; } return willUpdateStates; } componentDidMount() { this.scrollBarWidth = getScrollbarWidth(); this.originBodyWidth = document.body.style.width; if (this.props.visible) { this.foundation.beforeShow(); } } componentDidUpdate(prevProps, prevState) { if (prevProps.src !== this.props.src) { this.foundation.updateTimer(); } // hide => show if (!prevProps.visible && this.props.visible) { this.foundation.beforeShow(); } // show => hide if (prevProps.visible && !this.props.visible) { this.foundation.afterHide(); } } componentWillUnmount() { this.foundation.clearTimer(); } isInGroup() { return Boolean(this.context && this.context.isGroup); } render() { const { getPopupContainer, closable, zIndex, visible, className, style, infinite, zoomStep, crossOrigin, prevTip, nextTip, zoomInTip, zoomOutTip, rotateTip, downloadTip, adaptiveTip, originTip, showTooltip, disableDownload, renderPreviewMenu, renderHeader } = this.props; const { currentIndex, imgSrc, zoom, ratio, rotation, viewerVisible } = this.state; let wrapperStyle = { zIndex }; if (getPopupContainer) { wrapperStyle = { zIndex, position: "static" }; } const previewPrefixCls = `${prefixCls}-preview`; const previewWrapperCls = cls(previewPrefixCls, { [`${prefixCls}-hide`]: !visible, [`${previewPrefixCls}-popup`]: getPopupContainer }, className); const hideViewerCls = !viewerVisible ? `${previewPrefixCls}-hide` : ""; const total = imgSrc.length; const showPrev = total !== 1 && (infinite || currentIndex !== 0); const showNext = total !== 1 && (infinite || currentIndex !== total - 1); return visible && /*#__PURE__*/React.createElement(Portal, { getPopupContainer: getPopupContainer, style: wrapperStyle }, /*#__PURE__*/React.createElement("div", { className: previewWrapperCls, style: style, onMouseDown: this.handleMouseDown, onMouseUp: this.handleMouseUp, ref: this.registryImageWrapRef, onMouseMove: this.handleMouseMove }, /*#__PURE__*/React.createElement(Header, { ref: this.headerRef, className: cls(hideViewerCls), onClose: this.handlePreviewClose, renderHeader: renderHeader, closable: closable }), /*#__PURE__*/React.createElement(PreviewImage, { ref: this.imageRef, src: imgSrc[currentIndex], onZoom: this.handleZoomImage, disableDownload: disableDownload, setRatio: this.handleAdjustRatio, zoom: zoom, ratio: ratio, rotation: rotation, crossOrigin: crossOrigin, onError: this.onImageError, onLoad: this.onImageLoad }), showPrev && ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions React.createElement("div", { ref: this.leftIconRef, className: cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-prev`, hideViewerCls), onClick: () => this.handleSwitchImage("prev") }, /*#__PURE__*/React.createElement(IconArrowLeft, { size: "large" }))), showNext && ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions React.createElement("div", { ref: this.rightIconRef, className: cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-next`, hideViewerCls), onClick: () => this.handleSwitchImage("next") }, /*#__PURE__*/React.createElement(IconArrowRight, { size: "large" }))), /*#__PURE__*/React.createElement(Footer, { forwardRef: this.footerRef, className: hideViewerCls, totalNum: total, curPage: currentIndex + 1, disabledPrev: !showPrev, disabledNext: !showNext, zoom: zoom * 100, step: zoomStep * 100, showTooltip: showTooltip, ratio: ratio, prevTip: prevTip, nextTip: nextTip, zIndex: zIndex, zoomInTip: zoomInTip, zoomOutTip: zoomOutTip, rotateTip: rotateTip, downloadTip: downloadTip, disableDownload: disableDownload, adaptiveTip: adaptiveTip, originTip: originTip, onPrev: () => this.handleSwitchImage("prev"), onNext: () => this.handleSwitchImage("next"), onZoomIn: this.handleZoomImage, onZoomOut: this.handleZoomImage, onDownload: this.handleDownload, onRotate: this.handleRotateImage, onAdjustRatio: this.handleAdjustRatio, renderPreviewMenu: renderPreviewMenu }))); } } PreviewInner.contextType = PreviewContext; PreviewInner.propTypes = { style: PropTypes.object, className: PropTypes.string, visible: PropTypes.bool, src: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), currentIndex: PropTypes.number, defaultCurrentIndex: PropTypes.number, defaultVisible: PropTypes.bool, maskClosable: PropTypes.bool, closable: PropTypes.bool, zoomStep: PropTypes.number, infinite: PropTypes.bool, showTooltip: PropTypes.bool, closeOnEsc: PropTypes.bool, prevTip: PropTypes.string, nextTip: PropTypes.string, zoomInTip: PropTypes.string, zoomOutTip: PropTypes.string, downloadTip: PropTypes.string, adaptiveTip: PropTypes.string, originTip: PropTypes.string, lazyLoad: PropTypes.bool, preLoad: PropTypes.bool, preLoadGap: PropTypes.number, disableDownload: PropTypes.bool, viewerVisibleDelay: PropTypes.number, zIndex: PropTypes.number, maxZoom: PropTypes.number, minZoom: PropTypes.number, renderHeader: PropTypes.func, renderPreviewMenu: PropTypes.func, getPopupContainer: PropTypes.func, onVisibleChange: PropTypes.func, onChange: PropTypes.func, onClose: PropTypes.func, onZoomIn: PropTypes.func, onZoomOut: PropTypes.func, onPrev: PropTypes.func, onNext: PropTypes.func, onDownload: PropTypes.func, onRatioChange: PropTypes.func, onRotateLeft: PropTypes.func }; PreviewInner.defaultProps = { showTooltip: false, zoomStep: 0.1, infinite: false, closeOnEsc: true, lazyLoad: false, preLoad: true, preLoadGap: 2, zIndex: numbers.DEFAULT_Z_INDEX, maskClosable: true, viewerVisibleDelay: 10000, maxZoom: 5, minZoom: 0.1 };