前沿
写这份文档也是基于最近开发业务采集平台中使用 antd modal 组件,使用这个组件完成新增表单的弹窗,然后再做这个项目时候,觉得弹窗能够移动好像是个不错的用户体验。于是自己也就重新写了个这样的组件。
为什么写这个组件
1.antd modal 不能移动。
2.弹出动画在长期的开发过程视觉疲脑,虽然这个效果不错。
3.组件的一劳永逸是最为重要
想要什么效果
首先来看这样一张图很清楚一个标准的弹框这些是必不可少的
- 弹框的标题
- 弹窗内部子元素
- 弹窗的一个满屏背景
- 关闭 x 按钮
- 关闭按钮
- 确认/提交按钮
那么这些是必须有的属性,要做到高复用,我们应该让 1,2,5,6 作为参数传入;
这些是必须需要的,另外,可以让组件有更加好的延展性,也可以传入其他的参数比如: - modal 的高度
- modal 的宽度
- modal 的背景色
- 。。。等等
想要怎么使用
毋庸置疑好用才是封装组件的初衷通过整体思路希望是如下效果
<CanMoveModal
isShow={visible} // 通过参数判断开始是否展示
onOk={onOk} // 确认的回调
onCancel={onClose} //关闭按钮和'X'按钮的回调
title='这是标题' // 不传默认为空
okText='确认' // 不传默认 确认 文案
cancelText='关闭' // 不传默认 关闭 文案
>
<Input type="text"/>
</CanMoveModal>
封装
/**
* @desc: 自定义可移动模板/弹窗 Modal
* @name: CanMoveModal
* @author: ZM
*/
import React, { Component } from 'react';
import { Button, Icon } from 'antd';
import PropTypes from 'prop-types';
import Styles from './styles.less';
class CanMoveModal extends Component {
constructor(props) {
super(props);
const { isShow, width = 520 } = props;
const { clientWidth } = document.documentElement;
this.state = {
isShow,
pageX:(clientWidth - width)/2,
pageY:100,
moving: false,
};
}
componentWillReceiveProps({isShow}) {
this.setState({ isShow });
}
componentDidMount() {
this.resize();
window.addEventListener('resize', this.resize);
}
resize = () => {
const {clientWidth, clientHeight} = document.documentElement;
const modal = document.getElementById('modal');
if (modal) {
const pageY = (clientHeight - modal.offsetHeight) / 2;
const pageX = (clientWidth - modal.offsetWidth) / 2;
this.setState({ pageX, pageY});
}
}
onCancel = () => {
const {onCancel} = this.props;
if (onCancel) {
onCancel();
} else {
this.setState({ isShow: false });
}
}
open = () => {
this.setState({ isShow: true });
}
// 获取鼠标点击title时的坐标、title的坐标以及两者的位移
getPosition = (e) => {
// 标题DOM元素titleDom
const titleDom = e.target;
// titleDom的坐标
const X = titleDom.getBoundingClientRect().left;
const Y = titleDom.getBoundingClientRect().top;
// 鼠标点击的坐标
let mouseX = 0, mouseY = 0;
if (e.pageX || e.pageY) { // ff,chrome等浏览器
mouseX = e.pageX;
mouseY = e.pageY;
} else {
mouseX = e.clientX + document.body.scrollLeft - document.body.clientLeft;
mouseY = e.clientY + document.body.scrollTop - document.body.clientTop;
}
// 鼠标点击位置与modal的位移
const diffX = mouseX - X;
const diffY = mouseY - Y;
return {X, Y, mouseX, mouseY, diffX, diffY};
}
/**
* 鼠标按下,设置modal状态为可移动,并注册鼠标移动事件
* 计算鼠标按下时,指针所在位置与modal位置以及两者的差值
**/
onMouseDown = (e) => {
const position = this.getPosition(e);
window.onmousemove = this.onMouseMove;
this.setState({moving: true, diffX: position.diffX, diffY: position.diffY});
}
// 松开鼠标,设置modal状态为不可移动,
onMouseUp = () => {
this.setState({ moving: false });
}
// 鼠标移动重新设置modal的位置
onMouseMove = (e) => {
const { moving, diffX, diffY } = this.state;
if (moving) {
// 获取鼠标位置数据
const position = this.getPosition(e);
// 计算modal应该随鼠标移动到的坐标
const x = position.mouseX - diffX;
const y = position.mouseY - diffY;
// 窗口大小
const {clientWidth, clientHeight} = document.documentElement;
const modal = document.getElementById('modal');
if (modal) {
// 计算modal坐标的最大值
const maxHeight = clientHeight - modal.offsetHeight;
const maxWidth = clientWidth - modal.offsetWidth;
// 判断得出modal的最终位置,不得超出浏览器可见窗口
const left = x > 0 ? (x < maxWidth ? x : maxWidth) : 0;
const top = y > 0 ? (y < maxHeight ? y : maxHeight) : 0;
this.setState({pageX: left, pageY: top});
}
}
}
render() {
const {okText, cancelText, onOk, children, width = 520, height, title, animatedClass} = this.props;
const {isShow, pageX, pageY} = this.state;
const modal = (
<div className={`${Styles.modal_mask} animated ${animatedClass}`}>
<div
id='modal'
className={Styles.modal_body}
style={{
width,
height: height ? height : 'unset',
marginLeft: pageX,
marginTop: pageY,
}}>
<div className={Styles.modal_header}>
<span
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
className={Styles.modal_move}
></span>
<span className={Styles.modal_title}>{title ? title : null}</span>
<div className={Styles.modal_header_close} onClick={this.onCancel}>
<Icon type='close' />
</div>
</div>
<div className={Styles.modal_content}>
{children}
</div>
<div className={Styles.modal_footer}>
<div className={Styles.modal_footer_inner}>
<Button onClick={this.onCancel}>
{cancelText ? cancelText : '取消'}
</Button>
<Button type='primary' onClick={onOk.bind(null)} style={{marginLeft: '10px'}} >
{okText ? okText : '确定'}
</Button>
</div>
</div>
</div>
</div>
);
return isShow && modal
}
}
CanMoveModal.propTypes = {
isShow: PropTypes.bool,
title: PropTypes.string,
width: PropTypes.number,
height: PropTypes.any,
okText: PropTypes.string,
cancelText: PropTypes.string,
onCancel: PropTypes.func,
onOk: PropTypes.func,
animatedClass: PropTypes.string,
};
export default CanMoveModal;
/** 自定义modal相关样式 ***/
.modal_mask {
position: fixed;
overflow: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background-color: rgba(0, 0, 0, .65);
-webkit-overflow-scrolling: touch;
outline: 0;
}
.modal_body {
background: #fff;
position: relative;
border-radius: 5px;
min-width: 400px;
}
.modal_header {
position: relative;
width: 100%;
height: 48px;
line-height: 48px;
cursor: move;
text-align: left;
padding-left: 20px;
border-bottom: 1px solid #e3e3e3;
}
.modal_header_close {
width: 48px;
height: 48px;
cursor: pointer;
border: 0;
background: transparent;
position: absolute;
right: 0;
top: 0;
z-index: 10;
font-weight: 700;
line-height: 1;
text-decoration: none;
-webkit-transition: color .3s ease;
transition: color .3s ease;
color: rgba(0,0,0,.43);
outline: 0;
text-align: center;
line-height: 48px;
}
.modal_content {
top: 0;
width: 100%;
bottom: 50px;
overflow: auto;
padding: 10px 20px;
text-overflow: ellipsis;
background-color: #fff;
text-align: left;
max-height: 80vh;
min-height: 50px;
}
.modal_footer {
bottom: 0;
width: 100%;
height: 46px;
background: #fff;
border-top: 1px solid #e3e3e3;
border-radius: 0 0 5px 5px;
}
.modal_footer_inner {
right: 8px;
bottom: 8px;
position: absolute;
}
.modal_title {
margin: 0;
font-size: 14px;
line-height: 21px;
font-weight: 500;
color: rgba(0,0,0,.85)
}
.modal_move{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 50px;
user-select:none;
}
最后在加入了动画通过 animatedClass 参数来修改动画的效果
npm install animate.css
然后再引入 import ‘animate.css’;
通过再需要的 dom 元素上直接增加类名 class = ‘animated xxx’ xxx代表动画的效果,切记 class 类名一定需要加 animated