实现一个比ant功能更丰富的Modal组件

news2024/9/21 4:28:51

普通的modal组件如下:

我们写的modal额外支持,后面没有蒙版,并且Modal框能够拖拽

还支持渲染在文档流里,上面的都是fixed布局,我们这个正常渲染到文档下面:

render部分

 <RenderDialog{...restState}visible={visible}prefixCls={prefixCls}header={renderHeader}attach={attach}closeBtn={renderCloseIcon()}classPrefix={classPrefix}onClose={onClose}onConfirm={onConfirm}footer={footer === true ? defaultFooter() : footer}ref={dialogDom}/> 

大家记住这个RenderDialog,接下来都是上面传参的解释:

resetState: 是对象,一堆属性的集合,哪些属性呢,我们往下看

// 其实默认参数写到这里并不科学,因为react有个静态属性defaultProps属性支持合并propsconst [state, setState] = useSetState<DialogProps>({width: 520, // 默认宽度是520visible: false, // 默认visible是falsezIndex: 2500, // 默认zIndex 2500placement: 'center', // 默认渲染到屏幕中间mode: 'modal', // 默认的模式是modal是ant那种渲染结果,其他模式我们下面谈showOverlay: true, // 是否展示透明黑色蒙版destroyOnClose: false, // 关闭弹窗的时候是否销毁里面的内容draggable: false, // 是否能拖拽modalpreventScrollThrough: true, // 防止滚动穿透...props,}); 

restState在下面,除了state上某些属性。

 const { visible, // 控制对话框是否显示 attach, // 对话框挂载的节点,默认挂在组件本身的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body closeBtn, // 关闭按钮,可以自定义。值为 true 显示默认关闭按钮,值为 false 不显示关闭按钮。值类型为 string 则直接显示值,// 底部操作栏,默认会有“确认”和“取消”两个按钮。值为 true 显示默认操作按钮,值为 false 不显示任何内容,值类型为 Function 表示自定义底部内容 footer = true, // 如果“取消”按钮存在,则点击“取消”按钮时触发,同时触发关闭事件 onCancel = noop, // 如果“确认”按钮存在,则点击“确认”按钮时触发,或者键盘按下回车键时触发 onConfirm = noop, // 如果“确认”按钮存在,则点击“确认”按钮时触发,或者键盘按下回车键时触发 cancelBtn = cancelText, // 取消按钮,可自定义。值为 null 则不显示取消按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。 confirmBtn = confirmText, // 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。 onClose = noop, // 关闭事件,点击取消按钮、点击关闭按钮、点击蒙层、按下 ESC 等场景下触发 ...restState
 } = state; 

说了这么多,我们接着看RenderDialog组件上传入的属性。

prefixCls不讲了,是css属性前缀,一个字符串,接着看header属性被包装为renderHeader

const renderHeader = useMemo(() => {if (!state.header) return null;const iconMap = {info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />,warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />,error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />,success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />,};return (<div className={`${prefixCls}__header-content`}>{iconMap[state.theme]}{state.header}</div>);// eslint-disable-next-line react-hooks/exhaustive-deps}, [state.header, state.theme, prefixCls, classPrefix]); 

其实就是在header的文字前面多了一个icon,比如成功的弹窗如下:

接着看closeBtn属性

 const renderCloseIcon = () => {if (closeBtn === false) return null;if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />;return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />;}; 

这个是右上角关闭按钮的Icon,很简单,如果是false,什么都不许安然,如果是undefined或者true渲染这个icon。

好了,我们把整个代码放到下面,有代码注释,没写注释的是上面咋们已经讲过的内容,接着就要进入RenderDialog这个组件内部了。

import 的部分省略了

// 渲染 footer的button方法
const renderDialogButton = (btn: TdDialogProps['cancelBtn'], defaultProps: ButtonProps) => {let result = null;if (isString(btn)) {result = <Button {...defaultProps}>{btn}</Button>;}else if (isFunction(btn)) {result = btn();}return result;
};

const Dialog = forwardRef((props: DialogProps, ref: React.Ref<DialogInstance>) => {// 这部分忽略就好,用来获取全局配置的css前缀字符串const { classPrefix } = useConfig();// 这个也忽略,获取icon组件的const { CloseIcon, InfoCircleFilledIcon, CheckCircleFilledIcon } = useGlobalIcon({CloseIcon: TdCloseIcon,InfoCircleFilledIcon: TdInfoCircleFilledIcon,CheckCircleFilledIcon: TdCheckCircleFilledIcon,});// 用来引用dialog弹框的domconst dialogDom = useRef<HTMLDivElement>();const [state, setState] = useSetState<DialogProps>({width: 520,visible: false,zIndex: 2500,placement: 'center',mode: 'modal',showOverlay: true,destroyOnClose: false,draggable: false,preventScrollThrough: true,...props,});

 // 国际化有关的const [local, t] = useLocaleReceiver('dialog');const confirmText = t(local.confirm);const cancelText = t(local.cancel);const {visible,attach,closeBtn,footer = true,onCancel = noop,onConfirm = noop,cancelBtn = cancelText,confirmBtn = confirmText,onClose = noop,...restState} = state;useEffect(() => { setState((prevState) => ({...prevState,...props,}));}, [props, setState, isPlugin]);const prefixCls = `${classPrefix}-dialog`;const renderCloseIcon = () => {if (closeBtn === false) return null;if (closeBtn === true) return <CloseIcon style={{ verticalAlign: 'unset' }} />;return closeBtn || <CloseIcon style={{ verticalAlign: 'unset' }} />;};

 // 这里把一些外部方法暴露给调用者,只需要传入ref就可以获取React.useImperativeHandle(ref, () => ({show() {setState({ visible: true });},hide() {setState({ visible: false });},destroy() {setState({ visible: false, destroyOnClose: true });},update(newOptions) {setState((prevState) => ({...prevState,...(newOptions as DialogProps),}));},}));const renderHeader = useMemo(() => {if (!state.header) return null;const iconMap = {info: <InfoCircleFilledIcon className={`${classPrefix}-is-info`} />,warning: <InfoCircleFilledIcon className={`${classPrefix}-is-warning`} />,error: <InfoCircleFilledIcon className={`${classPrefix}-is-error`} />,success: <CheckCircleFilledIcon className={`${classPrefix}-is-success`} />,};return (<div className={`${prefixCls}__header-content`}>{iconMap[state.theme]}{state.header}</div>);// eslint-disable-next-line react-hooks/exhaustive-deps}, [state.header, state.theme, prefixCls, classPrefix]);

// 渲染footer的时候,点击取消按钮会用到const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {onCancel({ e });onClose({ e, trigger: 'cancel' });};

// 渲染footer的时候,点击确认按钮会用到const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {onConfirm({ e });};const defaultFooter = () => {const renderCancelBtn = renderDialogButton(cancelBtn, { variant: 'outline' });const renderConfirmBtn = renderDialogButton(confirmBtn, { theme: 'primary' });return (<>{renderCancelBtn &&React.cloneElement(renderCancelBtn, {onClick: handleCancel,...renderCancelBtn.props,})}{renderConfirmBtn &&React.cloneElement(renderConfirmBtn, {onClick: handleConfirm,...renderConfirmBtn.props,})}</>);};return (<RenderDialog{...restState}visible={visible}prefixCls={prefixCls}header={renderHeader}attach={attach}closeBtn={renderCloseIcon()}classPrefix={classPrefix}onClose={onClose}onConfirm={onConfirm}footer={footer === true ? defaultFooter() : footer}ref={dialogDom}/>);
});

Dialog.displayName = 'Dialog';
Dialog.defaultProps = dialogDefaultProps;

export default Dialog; 

接着,我们要渲染的部分其实很简单,包括

  • 背后的黑色蒙层
  • 弹框* 弹框的标题* 弹框的内容区域* 弹框的footer
  • 还需要弹框动画,比如zoom或者fade

渲染黑色蒙层

代码如下,很简单

 const renderMask = () => {let maskElement;if (showOverlay) {maskElement = (<CSSTransitionin={visible}appeartimeout={transitionTime}classNames={`${prefixCls}-fade`}mountOnEnterunmountOnExitnodeRef={maskRef}><div ref={maskRef} className={`${prefixCls}__mask`} /></CSSTransition>);}return maskElement;}; 

首先介绍一下CSSTransition,这是react-transition-group动画库的一个组件,用来帮助我们实现css动画的。 其中一些属性说明如下:

  • in: ture就是开始动画,false就是停止动画
  • appear:boolean,为 false 时当 CSSTransition 控件加载完毕后不执行动画,为 true 时控件加载完毕则立即执行动画。如果要组件初次渲染就有动画,则需要设成 true
  • timeout 动画时间
  • classNames:动画的类名,比如classNames:‘demo’,会自动在进入动画的时候帮你把类名改为 demo-enter-active, demo-enter-done, 在退出动画同样会有类名的改变。
  • mountOnEnter:一进来的时候不显示dom元素
  • unmountOnExit:boolean,为 true 时组件将移除处于隐藏状态的元素,为 false 时组件保持动画结束时的状态而不移除元素。一般要设成 true
  • nodeRef,获取蒙层的ref

蒙层主要靠css实现,我们看下css

 position: fixed;top: 0;left: 0;width: 100%;height: 100%;z-index: 1;background: var(--td-mask-active);pointer-events: auto; 

渲染弹框主体

也非常简单啊,我们把注释写在下面的代码里了,其中有一个需要小小注意的功能就是拖拽功能

// 渲染Dialog主体const renderDialog = () => {const dest: any = {};// 把width变为有px结尾的字符串if (props.width !== undefined) {dest.width = GetCSSValue(props.width);}// normal 场景下,需要设置 zindex 为auto 避免出现多个 dialog,normal 出现在最上层if (props.mode === 'normal') {dest.zIndex = 'auto';}// 获取footerconst footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null;// 获取headerconst { header } = props;// 获取Dialog bodyconst body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>;// 关闭按钮,可以自定义。值为 true 显示默认关闭按钮,值为 false 不显示关闭按钮。值类型为 string 则直接显示值,如:“关闭”。const closer = closeBtn && (<span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}>{closeBtn}</span>);const validWindow = typeof window === 'object';// 获取屏幕高度const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined;// 获取屏幕宽度const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined;// 设置styleconst style = { ...dest, ...props.style };let dialogOffset = { x: 0, y: 0 };// 拖拽代码实现部分const onDialogMove = (e: MouseEvent) => {// offsetWidth是指元素的宽 + padding + border的总和const { style, offsetWidth, offsetHeight } = dialog.current;// diffX是指弹框部分距离body左边部分let diffX = e.clientX - dialogOffset.x;let diffY = e.clientY - dialogOffset.y;// 拖拽上左边界限制if (diffX < 0) diffX = 0;if (diffY < 0) diffY = 0;// 右边的限制if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth;// 下边的限制if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight;style.position = 'absolute';style.left = `${diffX}px`;style.top = `${diffY}px`;};const onDialogMoveEnd = () => {// 恢复指针样式为默认,并且注销mousemove, mouseup事件dialog.current.style.cursor = 'default';document.removeEventListener('mousemove', onDialogMove);document.removeEventListener('mouseup', onDialogMoveEnd);};// 拖拽开始,对应mouseDown事件const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => {contentClickRef.current = true;// 阻止事件冒泡, mode === 'modeless才能拖拽if (canDraggable && e.currentTarget === e.target) {const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current;// 如果弹出框超出屏幕范围 不能进行拖拽if (offsetWidth > screenWidth || offsetHeight > screenHeight) return;// 拖拽样式设置为movedialog.current.style.cursor = 'move';// 计算鼠标 e.clientX是鼠标在屏幕的坐标,offsetLeft是Dialog主体跟body的距离// 所以e.clientX - offsetLeft就是鼠标在是Dialog主体上的横坐标const diffX = e.clientX - offsetLeft;const diffY = e.clientY - offsetTop;dialogOffset = {x: diffX,y: diffY,};// 此时把mousemove和mouseup事件也绑定一下,其实不建议绑定在这里直接操作domdocument.addEventListener('mousemove', onDialogMove);document.addEventListener('mouseup', onDialogMoveEnd);}};// 顶部定位实现const positionStyle: any = {};if (props.top) {const topValue = GetCSSValue(props.top);positionStyle.paddingTop = topValue;}// 此处获取定位方式 top 优先级较高 存在时 默认使用 top 定位const positionClass = classnames(`${prefixCls}__position`,{ [`${prefixCls}--top`]: !!props.top },`${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`,);// 然后就是用css去渲染header body和footerconst dialogElement = (<div className={isNormal ? '' : `${prefixCls}__wrap`}><div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}><divref={dialog}style={style}className={classnames(`${prefixCls}`, `${prefixCls}--default`)}onMouseDown={onDialogMoveStart}><div className={classnames(`${prefixCls}__header`)}>{header}{closer}</div>{body}{footer}</div></div></div>);return (<CSSTransitionin={props.visible}appearmountOnEnterunmountOnExit={destroyOnClose}timeout={transitionTime}classNames={`${prefixCls}-zoom`}onEntered={props.onOpened}onExited={onAnimateLeave}nodeRef={dialog}>{dialogElement}</CSSTransition>);}; 

我们这里贴一下css部分:

header:

.t-dialog__header {color: var(--td-text-color-primary);font: var(--td-font-title-medium);font-weight: 600;display: flex;align-items: flex-start;word-break: break-word;
} 

这里注意下:word-wrap:break-word

它会把整个单词看成一个整体,如果该行末端宽度不够显示整个单词,它会自动把整个单词放到下一行,而不会把单词截断掉的。

body

.t-dialog__body {padding: 16px 0;color: var(--td-text-color-secondary);font: var(--td-font-body-medium);overflow: auto;word-break: break-word;
} 

footer

 width: 100%;text-align: right;padding: 16px 0 0 0; 

好了,我们结合一下弹框和蒙层,看下render函数

const render = () => {// 。。。省略css部分// 如果不是 modal 模式 默认没有 mask 也就没有相关点击 mask 事件const dialog = (<div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}>{mode === 'modal' && renderMask()}{dialogBody} // 这里就是我们上面讲的renderDialog</div>);return dialog;}; 

设置body overflow:hiiden

为啥要设置body overflow:hiiden这个属性呢,你打开modal弹窗的时候,如果此时body还有滚动条,那么你滚动鼠标滚轮还可以向下滑动,但是一般情况下,我们打开弹框,是希望用户目标锁定在当前交互,此时最好不要允许用户滚动界面。

当然你也可以允许用户滚动,我们用一个preventScrollThrough参数控制。

先记住当前body的css样式,以及body的overflow的值,代码如下

 useLayoutEffect(() => {bodyOverflow.current = document.body.style.overflow;bodyCssTextRef.current = document.body.style.cssText;}, []); 
 const isModal = mode === 'modal';
useLayoutEffect(() => { // 只有modal数量小于1的时候才重置样式,因为可能出现多个弹框,那么关闭一个弹框就出现滚动条明显不对if (isModal) {const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);if (openDialogDom.length < 1) {document.body.style.cssText = bodyCssTextRef.current;}// 组件销毁后重置 body 样式return () => {if (isModal) {// 此处只能查询 mode 模式的 dialog 个数 因为 modeless 会点击透传 normal 是正常文档流const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);if (openDialogDom.length < 1) {document.body.style.cssText = bodyCssTextRef.current;document.body.style.overflow = bodyOverflow.current;}} };}, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]); 

上面的代码还有一个问题,就是我们需要preventScrollThrough这个参数去控制是否可以body滚动页面,这个也是算比ant更丰富的功能。

const isModal = mode === 'modal';
 useLayoutEffect(() => {// 处于显示态if (visible) {// isModal表示是否是普通弹框,就是带黑色蒙层的// bodyOverflow.current 引用的是body的overflow属性// preventScrollThrough是代表是否可以滚动body// !showInAttachedElement表示不挂载到其他dom上if (isModal && bodyOverflow.current !== 'hidden' && preventScrollThrough && !showInAttachedElement) {// 求出滚动条的宽度const scrollWidth = window.innerWidth - document.body.offsetWidth;// 减少回流if (bodyCssTextRef.current === '') {let bodyCssText = 'overflow: hidden;';if (scrollWidth > 0) {bodyCssText += `position: relative;width: calc(100% - ${scrollWidth}px);`;}document.body.style.cssText = bodyCssText;} else {if (scrollWidth > 0) {document.body.style.width = `calc(100% - ${scrollWidth}px)`;document.body.style.position = 'relative';}document.body.style.overflow = 'hidden';}}// 刚进页面就focus到弹框组件上if (wrap.current) {wrap.current.focus();}} else if (isModal) {const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);if (openDialogDom.length < 1) {document.body.style.cssText = bodyCssTextRef.current;}}// 组件销毁后重置 body 样式return () => {if (isModal) {// 此处只能查询 mode 模式的 dialog 个数 因为 modeless 会点击透传 normal 是正常文档流const openDialogDom = document.querySelectorAll(`${prefixCls}__mode`);if (openDialogDom.length < 1) {document.body.style.cssText = bodyCssTextRef.current;document.body.style.overflow = bodyOverflow.current;}} else {document.body.style.cssText = bodyCssTextRef.current;document.body.style.overflow = bodyOverflow.current;}};}, [preventScrollThrough, attach, visible, mode, isModal, showInAttachedElement, prefixCls]); 

其实还有一个逻辑,是把弹窗渲染到任意dom里,需要一个Portal组件,我们这里就不说了,后续将Popup或者叫trigger组件的时候我们讲吧。一篇文档内容太多不好消化。

好了,主逻辑已经写完了,很简单吧!

接下来看下完整代码,没有注释的部分是上面已经讲过的

省去了import

// 把css的数字转为有px结尾的字符串,,这里其实应该写到一个utils文件夹里,不应该跟主代码混在一起
function GetCSSValue(v: string | number) {return Number.isNaN(Number(v)) ? v : `${Number(v)}px`;
}

// 动画执行时间,这里其实应该写到一个constants文件里,不应该跟主代码混在一起
const transitionTime = 300;

const RenderDialog = forwardRef((props: RenderDialogProps, ref: React.Ref<HTMLDivElement>) => {// 这里不用看,跟国际化有关const [local] = useLocaleReceiver('dialog');const {prefixCls, attach, // 对话框挂载的节点,默认挂在组件本身的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.bodyvisible, // 控制对话框是否显示mode, // 对话框类型,有三种:模态对话框、非模态对话框和普通对话框。弹出「模态对话框」时,只能操作对话框里面的内容,不能操作其他内容。弹出「非模态对话框」时,则可以操作页面内所有内容。「普通对话框」是指没有脱离文档流的对话框,可以在这个基础上开发更多的插件zIndex, // 对话框层级,Web 侧样式默认为 2500,移动端和小程序样式默认为 1500showOverlay, // 是否显示遮罩层onEscKeydown = noop,// 按下 ESC 时触发事件onClosed = noop, // 对话框消失动画效果结束后触发onClose = noop, // 关闭事件,点击取消按钮、点击关闭按钮、点击蒙层、按下 ESC 等场景下触发onCloseBtnClick = noop, // 点击右上角关闭按钮时触发onOverlayClick = noop, // 如果蒙层存在,点击蒙层时触发onConfirm = noop, // 如果“确认”按钮存在,则点击“确认”按钮时触发,或者键盘按下回车键时触发preventScrollThrough, // 防止滚动穿透closeBtn, // 关闭按钮,可以自定义。值为 true 显示默认关闭按钮,值为 false 不显示关闭按钮。值类型为 string 则直接显示值,如:“关闭”。值类型为 TNode,则表示呈现自定义按钮示例closeOnEscKeydown, // 按下 ESC 时是否触发对话框关闭事件confirmOnEnter, // 是否在按下回车键时,触发确认事件closeOnOverlayClick, // 点击蒙层时是否触发关闭事件destroyOnClose, // 是否在关闭弹框的时候销毁子元素showInAttachedElement, // 仅在挂载元素中显示抽屉,默认在浏览器可视区域显示。父元素需要有定位属性,如:position: relative} = props;const wrap = useRef<HTMLDivElement>(); // 挂载到包裹弹框的dom上,包裹了好几层。。。const dialog = useRef<HTMLDivElement>(); // 引用弹窗domconst dialogPosition = useRef<HTMLDivElement>(); // 包裹弹窗,用于定位的dom引用const maskRef = useRef<HTMLDivElement>(); // 蒙层的dom引用const bodyOverflow = useRef<string>(); const bodyCssTextRef = useRef<string>();const contentClickRef = useRef(false);const isModal = mode === 'modal';const isNormal = mode === 'normal';const canDraggable = props.draggable && mode === 'modeless';const dialogOpenClass = `${prefixCls}__${mode}`;useLayoutEffect(() => {bodyOverflow.current = document.body.style.overflow;bodyCssTextRef.current = document.body.style.cssText;}, []);useLayoutEffect(() => {if (visible) {if (isModal && bodyOverflow.current <img src="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCodeif (e.key === 'Escape') {e.stopPropagation();onEscKeydown({ e });if (closeOnEscKeydown ?? local.closeOnEscKeydown) {onClose({ e, trigger: 'esc' });}} else if (e.key === 'Enter' || e.key === 'NumpadEnter') {// 回车键触发点击确认事件e.stopPropagation();if (confirmOnEnter) {onConfirm({ e });}}};// 渲染Dialog主体const renderDialog = () => {const dest: any = {};// 把width变为有px结尾的字符串if (props.width !== undefined) {dest.width = GetCSSValue(props.width);}// normal 场景下,需要设置 zindex 为auto 避免出现多个 dialog,normal 出现在最上层if (props.mode === 'normal') {dest.zIndex = 'auto';}// 获取footerconst footer = props.footer ? <div className={`${prefixCls}__footer`}>{props.footer}</div> : null;// 获取headerconst { header } = props;// 获取Dialog bodyconst body = <div className={`${prefixCls}__body`}>{props.body || props.children}</div>;// 关闭按钮,可以自定义。值为 true 显示默认关闭按钮,值为 false 不显示关闭按钮。值类型为 string 则直接显示值,如:“关闭”。const closer = closeBtn && (<span onClick={handleCloseBtnClick} className={`${prefixCls}__close`}>{closeBtn}</span>);const validWindow = typeof window === 'object';// 获取屏幕高度const screenHeight = validWindow ? window.innerHeight || document.documentElement.clientHeight : undefined;// 获取屏幕宽度const screenWidth = validWindow ? window.innerWidth || document.documentElement.clientWidth : undefined;// 设置styleconst style = { ...dest, ...props.style };let dialogOffset = { x: 0, y: 0 };// 拖拽代码实现部分const onDialogMove = (e: MouseEvent) => {// offsetWidth是指元素的宽 + padding + border的总和const { style, offsetWidth, offsetHeight } = dialog.current;// diffX是指弹框部分距离body左边部分let diffX = e.clientX - dialogOffset.x;let diffY = e.clientY - dialogOffset.y;// 拖拽上左边界限制if (diffX < 0) diffX = 0;if (diffY < 0) diffY = 0;// 右边的限制if (screenWidth - offsetWidth - diffX < 0) diffX = screenWidth - offsetWidth;// 下边的限制if (screenHeight - offsetHeight - diffY < 0) diffY = screenHeight - offsetHeight;style.position = 'absolute';style.left = `${diffX}px`;style.top = `${diffY}px`;};const onDialogMoveEnd = () => {// 恢复指针样式为默认,并且注销mousemove, mouseup事件dialog.current.style.cursor = 'default';document.removeEventListener('mousemove', onDialogMove);document.removeEventListener('mouseup', onDialogMoveEnd);};// 拖拽开始,对应mouseDown事件const onDialogMoveStart = (e: React.MouseEvent<HTMLDivElement>) => {contentClickRef.current = true;// 阻止事件冒泡, mode === 'modeless才能拖拽if (canDraggable && e.currentTarget === e.target) {const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = dialog.current;// 如果弹出框超出屏幕范围 不能进行拖拽if (offsetWidth > screenWidth || offsetHeight > screenHeight) return;// 拖拽样式设置为movedialog.current.style.cursor = 'move';// 计算鼠标 e.clientX是鼠标在屏幕的坐标,offsetLeft是Dialog主体跟body的距离// 所以e.clientX - offsetLeft就是鼠标在是Dialog主体上的横坐标const diffX = e.clientX - offsetLeft;const diffY = e.clientY - offsetTop;dialogOffset = {x: diffX,y: diffY,};// 此时把mousemove和mouseup事件也绑定一下,其实不建议绑定在这里直接操作domdocument.addEventListener('mousemove', onDialogMove);document.addEventListener('mouseup', onDialogMoveEnd);}};// 顶部定位实现const positionStyle: any = {};if (props.top) {const topValue = GetCSSValue(props.top);positionStyle.paddingTop = topValue;}// 此处获取定位方式 top 优先级较高 存在时 默认使用 top 定位const positionClass = classnames(`${prefixCls}__position`,{ [`${prefixCls}--top`]: !!props.top },`${props.placement && !props.top ? `${prefixCls}--${props.placement}` : ''}`,);const dialogElement = (<div className={isNormal ? '' : `${prefixCls}__wrap`}><div className={isNormal ? '' : positionClass} style={positionStyle} onClick={onMaskClick} ref={dialogPosition}><divref={dialog}style={style}className={classnames(`${prefixCls}`, `${prefixCls}--default`)}onMouseDown={onDialogMoveStart}><div className={classnames(`${prefixCls}__header`)}>{header}{closer}</div>{body}{footer}</div></div></div>);return (<CSSTransitionin={props.visible}appearmountOnEnterunmountOnExit={destroyOnClose}timeout={transitionTime}classNames={`${prefixCls}-zoom`}onEntered={props.onOpened}onExited={onAnimateLeave}nodeRef={dialog}>{dialogElement}</CSSTransition>);};const renderMask = () => {let maskElement;if (showOverlay) {maskElement = (<CSSTransitionin={visible}appeartimeout={transitionTime}classNames={`${prefixCls}-fade`}mountOnEnterunmountOnExitnodeRef={maskRef}><div ref={maskRef} className={`${prefixCls}__mask`} /></CSSTransition>);}return maskElement;};const render = () => {const style: CSSProperties = {};if (visible) {style.display = 'block';}const wrapStyle = {...style,zIndex,};const dialogBody = renderDialog();const wrapClass = classnames(props.className,`${prefixCls}__ctx`,!isNormal ? `${prefixCls}__ctx--fixed` : '',visible ? dialogOpenClass : '',isModal && showInAttachedElement ? `${prefixCls}__ctx--absolute` : '',props.mode === 'modeless' ? `${prefixCls}__ctx--modeless` : '',);// 如果不是 modal 模式 默认没有 mask 也就没有相关点击 mask 事件const dialog = (<div ref={wrap} className={wrapClass} style={wrapStyle} onKeyDown={handleKeyDown} tabIndex={0}>{mode === 'modal' && renderMask()}{dialogBody}</div>);let dom = null;if (visible || wrap.current) {// normal 模式 attach 无效if (attach === '' || isNormal) {dom = dialog;} else {dom = (<CSSTransitionin={visible}appeartimeout={transitionTime}mountOnEnterunmountOnExit={destroyOnClose}nodeRef={portalRef}><Portal attach={attach} ref={portalRef}>{dialog}</Portal></CSSTransition>);}}return dom;};return render()" style="margin: auto" />
});

RenderDialog.defaultProps = dialogDefaultProps;

export default RenderDialog; 

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/352112.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lesson5.2---Python 之 NumPy 切片索引和广播机制

一、切片和索引 ndarray 对象的内容可以通过索引或切片来访问和修改&#xff08;&#xff09;&#xff0c;与 Python 中 list 的切片操作一样。ndarray 数组可以基于 0 - n 的下标进行索引&#xff08;先行后列&#xff0c;都是从 0 开始&#xff09;。 区别在于&#xff1a;数…

代码随想录算法训练营第三十二天 | 122.买卖股票的最佳时机II,55. 跳跃游戏,45.跳跃游戏II

一、参考资料买卖股票的最佳时机IIhttps://programmercarl.com/0122.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BAII.html 跳跃游戏https://programmercarl.com/0055.%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F.html 跳跃游戏 IIhttps://pr…

金三银四必备软件测试必问面试题

初级软件测试必问面试题1、你的测试职业发展是什么&#xff1f;测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前 3 年积累测试经验&#xff0c;按如何做好测试工程…

【数据结构期末例题】

前言 本文是博主自己在准备学校数据结构考试时的总结&#xff0c;各个知识点都贴有对应的详细讲解文章以供大家参考&#xff1b;当然文中还有许许多多的截图&#xff0c;这些是博主对主要内容的摘取&#xff0c;对于那些基础较好的同学可以直接看截图&#xff0c;减少跳转对应文…

声呐学习笔记之波束成形

目录什么是波束什么是波束成形线阵数学推导(均匀排布)什么是波束 和光束一样&#xff0c;当所有波的传播方向都一致时&#xff0c;即形成了波束。工程师利用波束已经有相当久的历史。在二战中&#xff0c;工程师已经将波束利用在雷达中&#xff0c;雷达通过扫描波束方向来探测…

力扣-分数排名

大家好&#xff0c;我是空空star&#xff0c;本篇带你了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;178. 分数排名二、解题1.错误示范①提交SQL运行结果2.错误示范②提交SQL运行结果3.正确示范①提交SQL运行结果4.正确示范②提交SQL运行结果5.正确示范③提交…

全流程GMS地下水数值模拟技能培养及溶质运移反应问题深度解析实践技术

本次综合前期多次学习的效果及重点关注环节&#xff0c;系统性呈现地下水数值模拟软件GMS建模方法同时&#xff0c;建立与实践项目过程中的重点问题相融合&#xff0c;在教学中不仅强调学习三维地质结构建模、水文地质模型概化、边界条件设定、参数反演和模型校核等关键环节&am…

套娃式工具!用 AI 识别 AI ?#AI classifier

2022年以来&#xff0c;市面上就出现了不少 AI 生成文本的工具&#xff0c;尤其是 OpenAI 推出的 ChatGPT &#xff0c;不仅能够协助完成撰写邮件、视频脚本、文案、翻译、代码等任务&#xff0c;还能通过学习和理解人类的语言来进行对话&#xff0c;并根据聊天的上下文进行互动…

AI技术网关如何用于安全生产监测?有什么优势?

现代工业生产和运营的规模越来越庞大、系统和结构越来越复杂&#xff0c;现场的风险点多面广&#xff0c;给作业一线的安全监管带来极大的挑战。 针对工地、煤矿、危化品、加油站、烟花爆竹、电力等行业的安全生产监管场景&#xff0c;可以借助AI智能与物联网技术&#xff0c;…

4.1 Filter-policy

1. 实验目的 熟悉Filter-policy的应用场景掌握Filter-policy的配置方法2. 实验拓扑 Filter-policy实验拓扑如图4-5所示: 图4-5:Filter-policy 3. 实验步骤 (1) 网络连通性 R1的配置 <Huawei>system-vi…

点成分享|对于粘性液体该如何精准移液?

之前文章介绍移液器原理及分类时有说到&#xff0c;从移液器的使用原理来进行移液器的分类&#xff0c;大致就可分为空气置换式移液器和正向置换移液器&#xff08;即外置活塞式移液器&#xff09;。 对于粘性液体&#xff0c;特别是高粘度液体的移液操作&#xff0c;最好的方…

Vulnhub 渗透练习(四)—— Acid

环境搭建 环境下载 kail 和 靶机网络适配调成 Nat 模式&#xff0c;实在不行直接把网络适配还原默认值&#xff0c;再重试。 信息收集 主机扫描 没扫到&#xff0c;那可能端口很靠后&#xff0c;把所有端口全扫一遍。 发现 33447 端口。 扫描目录&#xff0c;没什么有用的…

代码随想录算法训练营第三十天 | 332.重新安排行程,51. N皇后,37. 解数独,总结

Day29 休息~一、参考资料重点&#xff01;&#xff01; 回溯算法总结篇https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E6%80%BB%E7%BB%93.html 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合排列问题&#xff1a;N个数按一定规则全排列&#xff0c;有几种排列方式…

【数字电路】数字电路的学习核心

文章目录前言一、电子电路知识体系二、数电的学习目标三、数字电路分析例子四、数字电路设计例子总结前言 用数字信号完成对数字量进行算术运算和逻辑运算的电路称为数字电路&#xff0c;或数字系统。由于它具有逻辑运算和逻辑处理功能&#xff0c;所以又称数字逻辑电路。现代…

2023美赛E题思路代码分析

2023美赛数学建模E题思路分析&#xff0c;更多的可以文末 E题&#xff1a;光污染 问题一&#xff1a;制定一个广泛适用的指标来确定一个地点的光污染风险水平。 首先我们要知道光污染以两种形式存在&#xff1a; 天空辉光&#xff08;也称为人造天空辉光、光穹或逃逸光&…

Exchange 2013升级以及域名绑定等若干问题

环境简介Exchange 2013服务器位于ad域中&#xff0c;系统为Windows server 2012 R2&#xff0c;其内部域名为&#xff1a;mail.ad.com一. Exchange客户端无法在浏览器中正常运行在域中部署Exchange服务器后&#xff0c;除了可以通过outlook、foxmail等邮件客户端来使用邮箱功能…

具有非线性动态行为的多车辆列队行驶问题的基于强化学习的方法

论文地址&#xff1a; Reinforcement Learning Based Approach for Multi-Vehicle Platooning Problem with Nonlinear Dynamic Behavior 摘要 协同智能交通系统领域的最新研究方向之一是车辆编队。研究人员专注于通过传统控制策略以及最先进的深度强化学习 (RL) 方法解决自动…

java易错题锦集四

effective java 不要再构造方法中启动任何线程 g new GameServer(); g.start();构造器无返回值&#xff0c;但是不能void修饰 字符串 String是包装类型吗&#xff1f;答案&#xff1a; 不是 对应的基本类型和包装类如下表&#xff1a; 基本数据类型 包装类 byte Byte bool…

使用开源 MaxKey 与 APISIX 网关保护你的 API

1. Apache APISIX介绍 Apache APISIX 是 Apache 软件基金会下的云原生 API 网关&#xff0c;它兼具动态、实时、高性能等特点&#xff0c;提供了负载均衡、动态上游、灰度发布&#xff08;金丝雀发布&#xff09;、服务熔断、身份认证、可观测性等丰富的流量管理功能。我们可以…

黑马redis学习记录:缓存

一、介绍 什么是缓存&#xff1f; 缓存(Cache)&#xff0c;就是数据交换的缓冲区&#xff0c;俗称的缓存就是缓冲区内的数据&#xff0c;一般从数据库中获取&#xff0c;存储于本地代码 缓存无处不在 为什么要使用缓存&#xff1f; 因为速度快,好用缓存数据存储于代码中,而…