实现一个比ant功能更丰富的Modal组件
普通的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的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
相关文章:
实现一个比ant功能更丰富的Modal组件
普通的modal组件如下: 我们写的modal额外支持,后面没有蒙版,并且Modal框能够拖拽 还支持渲染在文档流里,上面的都是fixed布局,我们这个正常渲染到文档下面: render部分 <RenderDialog{...restState}visi…...
2023美赛F题思路数据代码分享
文章目录赛题思路2023年美国大学生数学建模竞赛选题&论文一、关于选题二、关于论文格式三、关于论文提交四、论文提交流程注意不要手滑美赛F题思路数据代码【最新】赛题思路 (赛题出来以后第一时间在CSDN分享) 最新进度在文章最下方卡片,加入获取一手资源 202…...
Flutter如何与Native(Android)进行交互
前言 上一篇文章《Flutter混合开发:Android中如何启动Flutter》中我们介绍了如何在Native(Android项目)中启动Flutter,展示Flutter页面。但是在开发过程中,很多时候并不是简单的展示一个页面即可,还会涉及…...
数据库主从复制和读写分离
主从数据库和数据库集群的一些问题 数据库集群和主从数据库最本质的区别,其实也就是data-sharing和nothing-sharing的区别。集群是共享存储的。主从复制中没有任何共享。每台机器都是独立且完整的系统。 什么是主从复制? 主从复制,是用来建立一个和主数…...
Java并发编程面试题——线程安全(原子性、可见性、有序性)
文章目录一、原子性高频问题1.1 Java中如何实现线程安全?1.2 CAS底层实现1.3 CAS的常见问题1.4 四种引用类型 ThreadLocal的问题?二、可见性高频问题2.1 Java的内存模型2.2 保证可见性的方式2.3 volatile修饰引用数据类型2.4 有了MESI协议,为啥还有vol…...
DialogFragment内存泄露问题能不能一次性改好
孽缘 自DialogFragment在Android3.0之后作为一种特殊的Fragment引入,官方建议使用DialogFragment代替Dialog或者AllertDialog来实现弹框的功能,因为它可以更好的管理Dialog的生命周期以及可以更好复用。 然而建议虽好,实用须谨慎,…...
java学习--多线程
多线程 了解多线程 多线程是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。 并发和并行 并行:在同一时刻,有多个指令在CPU上同时执行并发࿱…...
90后阿里P7技术专家晒出工资单:狠补了这个,真香...
最近一哥们跟我聊天装逼,说他最近从阿里跳槽了,我问他跳出来拿了多少?哥们表示很得意,说跳槽到新公司一个月后发了工资,月入5万多,表示很满足!这样的高薪资着实让人羡慕,我猜这是税后…...
2023美赛C题:Wordle筛选算法
Wordle 规则介绍 Wordle 每天会更新一个5个字母的单词,在6次尝试中猜出单词就算成功。每个猜测必须是一个有效的单词(不能是不能组成单词的字母排列)。 每次猜测后,字母块的颜色会改变,颜色含义如下: 程…...
SpringBoot 集成 Kafka
SpringBoot 集成 Kafka1 安装 Kafka2 创建 Topic3 Java 创建 Topic4 SpringBoot 项目4.1 pom.xml4.2 application.yml4.3 KafkaApplication.java4.4 CustomizePartitioner.java4.5 KafkaInitialConfig.java4.6 SendMessageController.java5 测试1 安装 Kafka Docker 安装 Kafk…...
OpenCV 图像金字塔算子
本文是OpenCV图像视觉入门之路的第14篇文章,本文详细的介绍了图像金字塔算子的各种操作,例如:高斯金字塔算子 、拉普拉斯金字塔算子等操作。 高斯金字塔中的较高级别(低分辨率)是通过先用高斯核对图像进行卷积再删除偶…...
【自学Linux】Linux一切皆文件
Linux一切皆文件 Linux一切皆文件教程 Linux 中所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,就连套接字&…...
CUDA C++扩展的详细描述
CUDA C扩展的详细描述 文章目录CUDA C扩展的详细描述CUDA函数执行空间说明符B.1.1 \_\_global\_\_B.1.2 \_\_device\_\_B.1.3 \_\_host\_\_B.1.4 Undefined behaviorB.1.5 __noinline__ and __forceinline__B.2 Variable Memory Space SpecifiersB.2.1 \_\_device\_\_B.2.2. \_…...
为什么重写equals必须重写hashCode
关于这个问题,看了网上很多答案,感觉都参差不齐,没有答到要点,这次就记录一下! 首先我们为什么要重写equals?这个方法是用来干嘛的? public boolean equals (Object object&#x…...
< 每日小技巧:N个很棒的 Vue 开发技巧, 持续记录ing >
每日小技巧:6 个很棒的 Vue 开发技巧👉 ① Watch 妙用> watch的高级使用> 一个监听器触发多个方法> watch 监听多个变量👉 ② 自定义事件 $emit() 和 事件参数 $event👉 ③ 监听组件生命周期常规写法hook写法ὄ…...
数据结构与算法之二分查找分而治之思想
决定我们成为什么样人的,不是我们的能力,而是我们的选择。——《哈利波特与密室》二分查找是查找算法里面是很优秀的一个算法,特别是在有序的数组中,这种算法思想体现的淋漓尽致。一.题目描述及其要求请实现无重复数字的升序数组的…...
训练自己的中文word2vec(词向量)--skip-gram方法
训练自己的中文word2vec(词向量)–skip-gram方法 什么是词向量 将单词映射/嵌入(Embedding)到一个新的空间,形成词向量,以此来表示词的语义信息,在这个新的空间中,语义相同的单…...
ubuntu系统环境配置和常用软件安装
系统环境 修改文件夹名称为英文 参考链接 export LANGen_US xdg-user-dirs-gtk-update 常用软件安装 常用工具 ping 和ifconfig工具 sudo apt install -y net-tools inetutils-ping 截图软件 sudo apt install -y net-tools inetutils-ping flameshot 录屏 sudo apt-get i…...
【1139. 最大的以 1 为边界的正方形】
来源:力扣(LeetCode) 描述: 给你一个由若干 0 和 1 组成的二维网格 grid,请你找出边界全部由 1 组成的最大 正方形 子网格,并返回该子网格中的元素数量。如果不存在,则返回 0。 示例 1&#…...
windows11安装sqlserver2022报错
window11安装SQL Server 2022 报错 糟糕… 无法安装SQL Server (setup.exe)。此 SQL Server安装程序介质不支持此OS的语言,或没有SQL Server英语版本的安装文件。请使用匹配的特定语言SQL Server介质;或安装两个特定语言MUI,然后通过控制面板的区域设置…...
Python快速上手系列--日志模块--详解篇
前言本篇主要说说日志模块,在写自动化测试框架的时候我们就需要用到这个模块了,方便我们快速的定位错误,了解软件的运行情况,更加顺畅的调试程序。为什么要用到日志模块,直接print不就好了!那得写多少print…...
【THREE.JS学习(1)】绘制一个可以旋转、放缩的立方体
学习新技能,做一下笔记。在使用ThreeJS的时候,首先创建一个场景const scene new THREE.Scene();接着,创建一个相机其中,THREE.PerspectiveCamera()四个参数分别为:1.fov 相机视锥体竖直方向视野…...
数仓实战 - 滴滴出行
项目大致流程: 1、项目业务背景 1.1 目的 本案例将某出行打车的日志数据来进行数据分析,例如:我们需要统计某一天订单量是多少、预约订单与非预约订单的占比是多少、不同时段订单占比等 数据海量 – 大数据 hive比MySQL慢很多 1.2 项目架…...
python虚拟环境与环境变量
一、环境变量 1.环境变量 在命令行下,使用可执行文件,需要来到可执行文件的路径下执行 如果在任意路径下执行可执行文件,能够有响应,就需要在环境变量配置 2.设置环境变量 用户变量:当前用户登录到系统,…...
BeautifulSoup文档4-详细方法 | 用什么方法对文档树进行搜索?
4-详细方法 | 用什么方法对文档树进行搜索?1 过滤器1.1 字符串1.2 正则表达式1.3 列表1.4 True1.5 可以自定义方法2 find_all()2.1 参数原型2.2 name参数2.3 keyword 参数2.4 string 参数2.5 limit 参数2.6 recursive 参数3 find()4 find_parents()和find_parent()5…...
初识Tkinter界面设计
目录 前言 一、初识Tkinter 二、Label控件 三、Button控件 四、Entry控件 前言 本文简单介绍如何使用Python创建一个界面。 一、初识Tk...
软件测试面试题中的sql题目你会做吗?
目录 1.学生表 2.一道SQL语句面试题,关于group by表内容: 3.表中有A B C三列,用SQL语句实现:当A列大于B列时选择A列否则选择B列,当B列大于C列时选择B列否则选择C列 4. 5.姓名:name 课程:subject 分数&…...
VS实用调试技巧
一.什么是BUG🐛Bug一词的原意是虫子,而在电脑系统或程序中隐藏着的一些未被发现的缺陷或问题,人们也叫它"bug"。这是为什么呢?这就要追溯到一个程序员与飞蛾的故事了。Bug的创始人格蕾丝赫柏(Grace Murray H…...
通俗易懂理解三次握手、四次挥手(TCP)
文章目录1、通俗语言理解1.1 三次握手1.2 四次挥手2、进一步理解三次握手和四次挥手2.1 三次握手2.2 四次挥手1、通俗语言理解 1.1 三次握手 C:客户端 S:服务器端 第一次握手: C:在吗?我要和你建立连接。 第二次握手ÿ…...
1.1 什么是并发
1.1 什么是并发 并发:指两个或更多独立的活动同时发生。并发在生活中随处可见。我们可以一边走路一边说话,也可以两只手同时做不同的动作。 1.1.1 计算机系统中的并发 当我们提到计算机术语的“并发”,指的是在单个系统里同时执行多个独立…...
wordpress用户注册打文章/建网站需要多少钱和什么条件
网络策略介绍 默认情况下,Kubernetes 集群网络没任何网络限制,Pod 可以与任何其他 Pod 通信,在某些场景下就需要进行网络控制,减少网络攻击面,提高安全性,这就会用到网络策略。 网络策略(Netwo…...
wordpress改wp admin/软文营销名词解释
题目: 0,1,...n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字,求出这个圆圈里剩下的最后一个数字。 思路: 1、环形链表模拟圆圈 创建一个n个节点的环形链表,然后每次在这个链表中删除…...
网站做百度权重排名论坛/初学seo网站推广需要怎么做
merge merge 函数通过一个或多个键将数据集的行连接起来。 场景:针对同一个主键存在的两张包含不同特征的表,通过主键的链接,将两张表进行合并。合并之后,两张表的行数不增加,列数是两张表的列数之和。 def merge(left…...
登陆空间商网站/网站推广技巧
内外左右 内外左右 join字段 判断有不一定有,判断没有一定没有。 特点:判断有不一定有,判断没有一定没有。 多行输入,end键到每行最后, 每天的成交额 每年的成交额 new group 使用groupingsets case when&#x…...
连云港做网站优化/排行榜123网
一、将数据库中的Date格式的数据,或者指定日期的字符串格式化为想要的样式DATE_FORMAT (date, format)能根据格式串format 格式化日期或日期和时间值date,返回结果字符串。可用DATE_FORMAT( ) 来格式化DATE 或DATETIME 值,以便得到所希望的格…...
建设网站商城需要多少费用吗/新品上市怎么做宣传推广
一、在vue.config.js中添加配置 vueCli项目默认是不编译node_modules下的组件的,导致条件编译等功能失效,导致组件异常。 需要我们在vue.config.js文件中,添加dcloudio/uni-ui包的编译"transpileDependencies:[dcloudio/uni-ui]"…...