/* eslint-disable react/destructuring-assignment, prefer-const, @typescript-eslint/no-unused-vars */ import React, { CSSProperties, LegacyRef, ReactNode } from 'react'; import { cssClasses, strings } from '@douyinfe/semi-foundation/modal/constants'; import Button from '../button'; import ModalFoundation, { ModalAdapter, ModalProps, ModalState } from '@douyinfe/semi-foundation/modal/modalFoundation'; import ModalContent from './ModalContent'; import Portal from '../_portal'; import LocaleConsumer from '../locale/localeConsumer'; import cls from 'classnames'; import PropTypes from 'prop-types'; import { isUndefined } from 'lodash-es'; import '@douyinfe/semi-foundation/modal/modal.scss'; import { noop } from 'lodash-es'; import ContentTransition from './ModalTransition'; import BaseComponent from '../_base/baseComponent'; import confirm, { withConfirm, withError, withInfo, withSuccess, withWarning } from '../modal/confirm'; import { Locale } from '../locale/interface'; import useModal from '../modal/useModal'; import { ButtonProps } from '../button/Button'; export const destroyFns: any[] = []; export type ConfirmType = 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom'; export type Directions = 'ltr' | 'rtl'; export interface ModalReactProps extends ModalProps { cancelButtonProps?: ButtonProps; okButtonProps?: ButtonProps; bodyStyle?: CSSProperties; maskStyle?: CSSProperties; style?: CSSProperties; icon?: ReactNode; closeIcon?: ReactNode; title?: ReactNode; content?: ReactNode; footer?: ReactNode; header?: ReactNode; onCancel?: (e: React.MouseEvent) => void | Promise; onOk?: (e: React.MouseEvent) => void | Promise; } export { ModalState }; class Modal extends BaseComponent { static propTypes = { mask: PropTypes.bool, closable: PropTypes.bool, centered: PropTypes.bool, visible: PropTypes.bool, width: PropTypes.number, height: PropTypes.number, confirmLoading: PropTypes.bool, cancelLoading: PropTypes.bool, okText: PropTypes.string, okType: PropTypes.string, cancelText: PropTypes.string, maskClosable: PropTypes.bool, onCancel: PropTypes.func, onOk: PropTypes.func, afterClose: PropTypes.func, okButtonProps: PropTypes.object, cancelButtonProps: PropTypes.object, style: PropTypes.object, className: PropTypes.string, maskStyle: PropTypes.object, bodyStyle: PropTypes.object, zIndex: PropTypes.number, title: PropTypes.node, icon: PropTypes.node, header: PropTypes.node, footer: PropTypes.node, hasCancel: PropTypes.bool, motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]), children: PropTypes.node, getPopupContainer: PropTypes.func, getContainerContext: PropTypes.func, maskFixed: PropTypes.bool, closeIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), closeOnEsc: PropTypes.bool, size: PropTypes.oneOf(strings.SIZE), keepDOM: PropTypes.bool, lazyRender: PropTypes.bool, direction: PropTypes.oneOf(strings.directions), fullScreen: PropTypes.bool, }; static defaultProps = { zIndex: 1000, motion: true, mask: true, centered: false, closable: true, visible: false, confirmLoading: false, cancelLoading: false, okType: 'primary', maskClosable: true, hasCancel: true, onCancel: noop, onOk: noop, afterClose: noop, maskFixed: false, closeOnEsc: false, size: 'small', keepDOM: false, lazyRender: true, fullScreen: false, }; static useModal = useModal; foundation: ModalFoundation; private readonly modalRef: LegacyRef; private bodyOverflow: string; private scrollBarWidth: number; private originBodyWith: string; private _active: boolean; constructor(props: ModalReactProps) { super(props); this.state = { hidden: !props.visible, isFullScreen: props.fullScreen, }; this.foundation = new ModalFoundation(this.adapter); this.modalRef = React.createRef(); this.bodyOverflow = ''; this.scrollBarWidth = 0; this.originBodyWith = '100%'; } get adapter(): ModalAdapter { return { ...super.adapter, getProps: () => this.props, 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.originBodyWith || '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.originBodyWith; } }, notifyCancel: (e: React.MouseEvent) => { this.props.onCancel(e); }, notifyOk: (e: React.MouseEvent) => { this.props.onOk(e); }, notifyClose: () => { this.props.afterClose(); }, toggleHidden: (hidden: boolean) => { if (hidden !== this.state.hidden) { this.setState({ hidden }); } }, notifyFullScreen: (isFullScreen: boolean) => { if (isFullScreen !== this.state.isFullScreen) { this.setState({ isFullScreen }); } }, }; } static getDerivedStateFromProps(props: ModalReactProps, prevState: ModalState) { const newState: Partial = {}; if (props.visible && prevState.hidden) { newState.hidden = false; } if (props.fullScreen !== prevState.isFullScreen) { newState.isFullScreen = props.fullScreen; } // if not using animation, need to update hidden state from props if (!props.visible && !props.motion && !prevState.hidden) { newState.hidden = true; } return newState; } static getScrollbarWidth() { if (Object.prototype.toString.call(globalThis) === '[object Window]') { return window.innerWidth - document.documentElement.clientWidth; } return 0; } static info = function (props: ModalReactProps) { return confirm(withInfo(props)); }; static success = function (props: ModalReactProps) { return confirm(withSuccess(props)); }; static error = function (props: ModalReactProps) { return confirm(withError(props)); }; static warning = function (props: ModalReactProps) { return confirm(withWarning(props)); }; static confirm = function (props: ModalReactProps) { return confirm(withConfirm(props)); }; static destroyAll = function destroyAllFn() { while (destroyFns.length) { const close = destroyFns.pop(); if (close) { close(); } } }; componentDidMount() { this.scrollBarWidth = Modal.getScrollbarWidth(); this.originBodyWith = document.body.style.width; if (this.props.visible) { this.foundation.beforeShow(); this._active = this._active || this.props.visible; } } componentDidUpdate(prevProps: ModalReactProps, prevState: ModalState, snapshot: any) { // hide => show if (!prevProps.visible && this.props.visible) { this.foundation.beforeShow(); } // show => hide if (prevProps.visible && !this.props.visible) { this.foundation.afterHide(); } } componentWillUnmount() { if (this.props.visible) { this.foundation.destroy(); } } handleCancel = (e: React.MouseEvent) => { this.foundation.handleCancel(e); }; handleOk = (e: React.MouseEvent) => { this.foundation.handleOk(e); }; renderFooter = (): ReactNode => { const { okText, okType, cancelText, confirmLoading, cancelLoading, hasCancel, } = this.props; const getCancelButton = (locale: Locale['Modal']) => { if (!hasCancel) { return null; } else { return ( ); } }; return ( {(locale: Locale['Modal'], localeCode: Locale['code']) => (
{getCancelButton(locale)}
)}
); }; getDialog = () => { const { footer, ...restProps } = this.props; const renderFooter = 'footer' in this.props ? footer : this.renderFooter(); return ; }; renderDialogWithTransition = ({ opacity, scale, }: { opacity?: CSSProperties['opacity']; scale?: number; } = {}) => { let { footer, className, motion, maskStyle: maskStyleFromProps, keepDOM, style: styleFromProps, zIndex, getPopupContainer, ...restProps } = this.props; let maskStyle = maskStyleFromProps; let style = styleFromProps; const renderFooter = 'footer' in this.props ? footer : this.renderFooter(); if (this.props.centered) { style = { transform: 'translateY(-50%)', top: '50%', ...style, }; } if (!isUndefined(opacity)) { maskStyle = { opacity, ...maskStyle }; style = { opacity, ...style }; } if (!isUndefined(scale)) { style = { transform: `scale(${scale})`, ...style }; if (this.props.centered) { style = { ...style, transform: `scale(${scale}) translateY(-${50 / scale}%)`, }; } } let wrapperStyle: { zIndex?: CSSProperties['zIndex']; position?: CSSProperties['position']; } = { zIndex, }; if (getPopupContainer) { wrapperStyle = { zIndex, position: 'static', }; } const classList = cls(className, { [`${cssClasses.DIALOG}-hidden`]: keepDOM && this.state.hidden, }); return ( ); }; render() { const { visible, keepDOM, lazyRender, } = this.props; this._active = this._active || visible; const shouldRender = (visible || keepDOM) && (!lazyRender || this._active); const mergedMotion = this.foundation.getMergedMotion(); if (mergedMotion) { return ( {shouldRender ? this.renderDialogWithTransition : null} ); } if (shouldRender) { return this.renderDialogWithTransition(); } return null; } } export default Modal;