You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/web/src/components/Modal.tsx

192 lines
4.2 KiB
TypeScript

import React, { useCallback, useContext, useState } from 'react';
import _isFunction from 'lodash/isFunction';
import _isNil from 'lodash/isNil';
import _last from 'lodash/last';
import _pull from 'lodash/pull';
import _isString from 'lodash/isString';
import _noop from 'lodash/noop';
import { PortalAdd, PortalRemove } from './Portal';
import { Typography } from 'antd';
import { Icon } from '@/components/Icon';
import { CSSTransition } from 'react-transition-group';
import clsx from 'clsx';
import { useIsMobile } from '@/hooks/useIsMobile';
import './Modal.less';
import { stopPropagation } from '@/utils/dom-helper';
const transitionEndListener = (node: HTMLElement, done: () => void) =>
node.addEventListener('transitionend', done, false);
/**
* 模态框
*/
const ModalContext = React.createContext<{
closeModal: () => void;
}>({
closeModal: _noop,
});
interface ModalProps {
visible?: boolean;
onChangeVisible?: (visible: boolean) => void;
/**
* 是否显示右上角的关闭按钮
* @default false
*/
closable?: boolean;
/**
* 遮罩层是否可关闭
*/
maskClosable?: boolean;
}
export const Modal: React.FC<ModalProps> = React.memo((props) => {
const {
visible,
onChangeVisible,
closable = false,
maskClosable = true,
} = props;
const [showing, setShowing] = useState(true);
const closeModal = useCallback(() => {
setShowing(false);
}, []);
const handleClose = useCallback(() => {
if (maskClosable === false) {
return;
}
closeModal();
}, [maskClosable, closeModal]);
if (visible === false) {
return null;
}
return (
<CSSTransition
in={showing}
classNames="modal-anim"
timeout={200}
addEndListener={transitionEndListener}
onExited={() => {
if (showing === false && _isFunction(onChangeVisible)) {
onChangeVisible(false);
}
}}
appear={true}
>
<div
className="absolute left-0 right-0 top-0 bottom-0 bg-black bg-opacity-60 flex justify-center items-center z-10"
onClick={handleClose}
data-tc-role="modal-mask"
>
<ModalContext.Provider value={{ closeModal }}>
{/* Inner */}
<div
className="modal-inner bg-content-light dark:bg-content-dark rounded overflow-auto relative z-10"
style={{ maxHeight: '80vh', maxWidth: '80vw' }}
onClick={stopPropagation}
data-tc-role="modal"
>
{closable === true && (
<Icon
className="absolute right-2.5 top-3.5 text-xl z-10 cursor-pointer"
icon="mdi:close"
onClick={closeModal}
/>
)}
{props.children}
</div>
</ModalContext.Provider>
</div>
</CSSTransition>
);
});
Modal.displayName = 'Modal';
const modelKeyStack: number[] = [];
/**
* 关闭Modal
*/
export function closeModal(key?: number): void {
if (_isNil(key)) {
key = _last(modelKeyStack);
}
if (typeof key === 'number') {
_pull(modelKeyStack, key);
PortalRemove(key);
}
}
/**
* 打开新的Modal
*/
export function openModal(
content: React.ReactNode,
props?: Pick<ModalProps, 'closable' | 'maskClosable'>
): number {
const key = PortalAdd(
<Modal
{...props}
visible={true}
onChangeVisible={(visible) => {
if (visible === false) {
closeModal(key);
}
}}
>
{content}
</Modal>
);
modelKeyStack.push(key);
return key;
}
/**
* 获取modal上下文
*/
export function useModalContext() {
const { closeModal } = useContext(ModalContext);
return { closeModal };
}
/**
* 标准模态框包装器
*/
export const ModalWrapper: React.FC<{
title?: string;
className?: string;
style?: React.CSSProperties;
}> = React.memo((props) => {
const isMobile = useIsMobile();
const title = _isString(props.title) ? (
<Typography.Title level={4} className="text-center mb-4">
{props.title}
</Typography.Title>
) : null;
return (
<div
className={clsx('p-4', props.className)}
style={{ minWidth: isMobile ? 290 : 420, ...props.style }}
>
{title}
{props.children}
</div>
);
});
ModalWrapper.displayName = 'ModalWrapper';