我试着按照 benjycui 的思路用javascript的drag api写了一个比较粗糙的Wrapper,通过修改top样式实现了纵向拖拽Modal。供参考。
class DraggableModal extends React.Component {
constructor(props) {
super();
const top = (props.style && props.style.top) ? props.style.top : 0;
this.state = { top };
handleDrag = (e) => {
this.setState({ top: e.clientY - 32 });
render() {
const { title, style, ...otherProps } = this.props;
const newTitle = (
style={{ background: "#F0F0F0" }}
draggable={true}
onDrag={this.handleDrag}
onDragEnd={this.handleDrag}>
{title}
</div>);
const newStyle = { ...style, top: this.state.top };
return (
<Modal {...otherProps} title={newTitle} style={newStyle} />
afc163, chendonghai1982, superlbr, lc-stevenzhang, dazer007, duoduoObama, macc6579, ryoman81, paidaxing-11, and lichenghuan reacted with thumbs up emoji
Crown1991 reacted with thumbs down emoji
lc-stevenzhang reacted with laugh emoji
lc-stevenzhang reacted with hooray emoji
lc-stevenzhang reacted with heart emoji
All reactions
@chendonghai1982 如下做就可以了实现了,你可以把代码处理的更好看一些
this.modalDom = document.getElementsByClassName(
"ant-modal"
)[(document.getElementsByClassName("ant-modal")).length-1];
import React from 'react';
import Draggable from 'react-draggable';
import { Button, Spin, Icon, message } from 'antd';
import DialogBus from './dialog-bus'
import '../styles/dialog.css'
import Utils from '../utils/utils';
import { LocaleProvider } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import copy from 'copy-to-clipboard';
弹出多窗口对话框,用法模仿 ant design中modal对话框来的
项目需求:
房源列表上面每点一个就自动产生一个对话框来,不用开始就写在jsx代码里,注意这个:::"divId":::是固定的名称,不要写成别的要不然到时候移除不了这个新产生的div
1、这时关闭事件不用自己去定义,里面自动处理了关闭事件
2、定义一个组件的时候一定要把这个newdiv带下去, 如定义一个xxx组件:
xxx
不传这个newdiv到时候关闭不了
3、如果手工要关闭的就调用: this.refs.informDialog.onClose(); 这里还有一点注意像房源他自己写了取消按钮关闭按钮那么一定要调用我这个方法来关闭窗口,要不然窗口还一直存在,除非你用自带的按钮就不用管了
不是自动用代码来生成对话框直接写到jsx上去,用法跟ant design modal一样, 注意:这时要定义onCancel方法来关闭窗口,上面的那种创建div元素不用去定义
还有一些其他属性见最下面的参数
列表上面弹出多个窗口可以参照房源列表来写,一定要那样写,要不然会有问题
如房源代表调用:
let divId = "HouseFormSpan" + record.id; // 这个id一定要唯一的,因为到时候要拿这个作为底部排序用还要删除
DialogBus.openDialog()
// 注意由于你这里是自己写的关闭按钮事件,一定要调用我的方法来关闭,要不然清除不了创建的对话框窗口,像张超房源取消窗口就是个例子
this.refs.houseDialog.onClose();
let zIndex = 1;
export default class Dialog extends React.Component {
constructor(props) {
super(props);
this.isMin = false;
this.dialogDefaultY = "39px";
this.clientWidth = document.body.clientWidth;
this.clientHeight = document.body.clientHeight;
const {dialogY} = this.props;
this.state = { activeDrags: 0, confirmLoading: false, visible: this.props.visible == undefined ? false : this.props.visible,
height: this.props.height, contPadding: "5px 10px",
dialogStyle: {margin: "0 auto", top: undefined == dialogY ? this.dialogDefaultY: (dialogY+"px"), width: this.props.width},
titleContStyle: {height: "40px", lineHeight: "40px"},
titleStyle: {paddingLeft: "16px", fontSize: "14px"},
iconStyle: {marginRight: "16px"},
controlledPosition: null,
footStyle: {}, maxText: "+", maxTitle: "最大化"};
// 这种是直接创建div元素加到body上去
if (divId != undefined) {
this.setState({visible: false});// 直接创建div元素加到body上去了,要通过内部把他隐藏起来
if (!this.isMin) {
// 当前已经最大化了,准备关闭窗口
DialogBus.closeDialog(this.divId);
} else {
// 当前最小化了,在底部最小化的时候直接关闭,这时候要先移除然后再重新排位置
DialogBus.closeDialog(this.divId);
DialogBus.sortDialog();
if (this.props.afterClose != undefined) {
this.props.afterClose();
// 开始拖动
onStart = (e) => {
// 最小化了禁止拖动,注意要返回false才会生效
if (this.isMin) {
return false;
// controlledPosition: 拖动的时候一定要把他设置为null,要不然一直会返回原点,因为前最大化定死了他,而每次拖动会走render方法,然后获取到这个值又重新返回去了
this.setState({ activeDrags: ++this.state.activeDrags,controlledPosition: null });
// 把当前点的放在最上面来,其他的放在后面
const node = e.target.parentNode.parentNode.parentNode;
const elem = document.getElementsByClassName('m-dialog');
this.setZindex(node, elem);
setWidthHeight = (width, height) =>{
// 停止拖动
onStop = () => { this.setState({ activeDrags: --this.state.activeDrags });}
// 最大化窗口,通知详情用到
onMax = () => {
const {height} = this.props;
const {dialogStyle} = this.state;
if (this.state.maxText == "+") {
dialogStyle.width = this.clientWidth;
dialogStyle.top = "0px";
this.setState({dialogStyle, height: this.clientHeight, maxText: "-", maxTitle: "最小化"});
} else {
dialogStyle.width = this.props.width;
dialogStyle.top = this.dialogDefaultY;
this.setState({dialogStyle, height: height, maxText: "+", maxTitle: "最大化"});
setZindex(node, elem){
node.style.zIndex = 99;
const count = elem.length;
const nodeTransform = node.style.transform;
for (let i=0; i< count; i++) {
if (elem[i].style.transform != nodeTransform) {
elem[i].style.zIndex = 1;
// 从底部最小化到最大化
maxDialog = (width, height) => {
DialogBus.minToMaxDialog(this.divId);
// 把当前最大化的放在最上面来,其他的放在后面
/*const node = document.getElementById(this.divId);
const elem = document.getElementsByClassName('myDialog');
node.style.zIndex = 99;
console.log("当前最大化的+++++的元素")
console.log(node.id);
const count = elem.length;
for (let i=0; i< count; i++) {
if (elem[i].id != node.id) {
console.log("找到不相等的元素为:")
console.log(elem[i].id);
elem[i].style.zIndex = 1;
const node = document.getElementById(this.divId).childNodes[0].childNodes[0].childNodes[0];
const elem = document.getElementsByClassName('m-dialog');
this.setZindex(node, elem);
this.setState({
dialogStyle: {margin: "0 auto", top: "55px", width: width,},
iconStyle: {paddingRight: "16px"},
height: height,
contPadding: "5px 10px",
footStyle: {},
titleStyle: {paddingLeft: "16px", fontSize: "14px"},
controlledPosition: {x:0, y:Utils.GetRandomNum(1,100)},/* 注意点:如果这里不重归于0会产生一个问题到时候最大化返回的时候transform: translate(leftX, 0px);他的X轴会变动会取到最小化时的controlledPosition X轴,后面Y轴加了一个随机数就是防止他重叠在一起*/
titleContStyle: {height: "40px", lineHeight: "40px"}
// 最小化
onMin = () => {
const {width, height} = this.props;
if (this.state.dialogStyle.width != DialogBus.dialogWidth) {
// 最小化了
this.isMin = true;
this.sortBottom(DialogBus.getDialogLeft(this.divId, this, width, height));
} else {
// 最大化
this.isMin = false;
this.maxDialog(width, height);
// 最小化排列底部
sortBottom = (leftX) => {
this.setState({
dialogStyle: {bottom: "0px", width: DialogBus.dialogWidth},
iconStyle: {paddingRight: "6px"},
height: 0,
contPadding: "0px",
footStyle: {height: "0px", overflow: "hidden"},
titleStyle: {width: DialogBus.dialogTitleWidth, fontSize: "12px"},
controlledPosition: {x:leftX, y:0},
titleContStyle: {height: "30px", lineHeight: "30px"}
// 注意这个隐藏起来一定要到外部去操作不能直接到dialog里面设置this.setState({ visible: false});因为这样设置的话,父组件或其他组件调用了setState方法都会执行这里的生命周期
// 而且这时候nextProps.visible他的值一直没有改变,所以这个visilbe从那里设置过来的,那么隐藏也要从那里去设置
componentWillReceiveProps = (nextProps) => {
if (nextProps.confirmLoading != undefined) {
this.setState({ confirmLoading: nextProps.confirmLoading });
if (nextProps.visible != undefined) {
this.setState({ visible: nextProps.visible });
/* 下面这个做处理判断是有时业务需求需要动态来改变窗口大小,而不是一开始是怎么样就一直不变了 */
// 通过旧的props.width是否与新的nextProps.width相同来绝定是否要setState来改变宽度从而触发render改变窗口大小,当一个页面有多个组件时,用了this.props.width来做判断是不会影响到其他组件的
if (nextProps.width != undefined && this.props.width != nextProps.width) {
this.setState({width: nextProps.width});
// 通过旧的props.height是否与新的nextProps.height相同来绝定是否要setState来改变高度从而触发render改变窗口大小,当一个页面有多个组件时,用了this.props.width来做判断是不会影响到其他组件的
if (nextProps.height != undefined && this.props.height != nextProps.height) {
this.setState({height: nextProps.height});
componentDidMount = () => {
// 当新点一个窗口的时候,把他们全部设置为1,这样最后点的一个弹出窗口就会在最前面,因为他创建的元素在body最后面,所以设置为1还是生效的
const elem = document.getElementsByClassName('m-dialog');
const count = elem.length;
for (let i=0; i< count; i++) { elem[i].style.zIndex = 1;}
const {divId} = this.props;
if (undefined != divId) {
this.divId = divId;
} else {
this.divId = Utils.rnd(1,999999).toString(); // 有一种是直接XML布局的得不到那个ID,所以这里自动随机产生一个
onTitleClick = (title)=>{
let msg = "标题复制成功";
if (title.indexOf(",") != -1) {
const array = title.split(",");
title = array[0];
msg = "房源编号复制成功!";
copy(title);
message.info(msg);
render() {
const {visible, height, footStyle, confirmLoading, maxText, maxTitle, contPadding,dialogStyle,titleStyle,titleContStyle,iconStyle,controlledPosition} = this.state;
const {onCancel, cancelText, onOk, okText, title, children, footer, footLeftInfo, footBtn, isMax, isMin} = this.props;
// 默认按钮
const btns = [<Button key="0" onClick={onCancel == undefined ? this.onClose : onCancel}>{cancelText}</Button>,
<Button key="1" type="primary" onClick={onOk == undefined ? this.onClose : onOk}>{okText}</Button>];
return (
{visible ?
<Draggable handle=".md-dialog-title" onStart={this.onStart} onStop={this.onStop} position={controlledPosition}>
<div className="m-dialog" style={dialogStyle}>
<Spin spinning={confirmLoading}>
<div className="md-dialog-title" style={titleContStyle}>
<div className="md-dialog-title-font" style={titleStyle} onDoubleClick={()=>{this.onTitleClick(title)}}>{title}</div>
<span className="btn" onClick={this.onClose}>
<i className="md-dialog-title-iconfont" style={iconStyle} title="关闭"><Icon type="close" /></i>
</span>
{isMax ? <span className="btn" onClick={this.onMax}><i className="md-dialog-title-iconfont" style={iconStyle} title={maxTitle}>{maxText}</i></span> : null}
{isMin ? <span className="btn" onClick={this.onMin}><i className="md-dialog-title-iconfont" style={iconStyle} ><Icon type="minus" /></i></span> : null}
<LocaleProvider locale={zhCN}>
<div className="md-dialog-content" style={{height: height, padding: contPadding}}>
{children}
</LocaleProvider>
{footer != null ?
<div className="md-dialog-foot" style={footStyle}>
<span className="md-dialog-foot-left">{footLeftInfo}</span>
<span className="md-dialog-foot-right">{footBtn == undefined ? btns : footBtn}</span>
:null}
</Spin>
</Draggable> : null}
Dialog.propTypes = {
divId: React.PropTypes.string, /* 动态代码来产生的对话框到时候要把产生新的div移除掉 /
width: React.PropTypes.number, / 宽度 /
height: React.PropTypes.number,/ 是指中间内容区域高度,初始值为260,头部和脚部除外 /
title: React.PropTypes.string, / 标题 /
okText: React.PropTypes.string, / 确认按钮文字 /
cancelText: React.PropTypes.string, / 取消按钮文字 /
onOk: React.PropTypes.func, / 点击确定回调 /
onCancel: React.PropTypes.func, / 取消按钮的回调 /
afterClose: React.PropTypes.func, / 完全关闭后的回调 /
confirmLoading: React.PropTypes.bool, / 确定按钮 loading /
footLeftInfo: React.PropTypes.string, / 底部左边文件信息 */
isMax: React.PropTypes.bool,
isMin: React.PropTypes.bool,
visible: React.PropTypes.bool,
Dialog.defaultProps = {
width: 500, /* 对话框默认宽度 /
height: 260, / 是指中间内容区域高度,初始值为260,头部和脚部除外 /
title: "", / 标题 /
okText: "保存", / 确认按钮文字 /
cancelText: "取消", / 取消按钮文字 /
confirmLoading: false,
footer: {}, / 不想显示底部下面的按纽直接把该属性设置为null /
footLeftInfo: "", / 底部左边文件信息 /
isMax: false,/ 是否需要最大化 /
isMin: true, / 是否需要最小化 */
visible: true
// 对话框的一些逻辑
import ReactDOM from 'react-dom';
import { Modal } from 'antd';
import TipMsgStore from '../stores/tip-msg-store';
export default class DialogBus {
static dialogWidth = 150;
static dialogTitleWidth = 94; // 这个宽度要跟上面dialogWidth配合来的,要不然最小化看不到
static dialogArray = []; // 存取对话框
//static dialogLeftCount = 0; // 存取left坐标
// 得到x轴
static getDialogLeft(divId, curObj, width, height) {
如果已经最小化了再点就自动最大化了,所以这里没有必要处理
if (DialogBus.isExit(index)) {
// 已经存在了
currObj.setState({dialogStyle: {margin: "0 auto", top: "55px"}});
return
let count = DialogBus.dialogArray.length;
let left = DialogBus.dialogWidth * count;
left += count * 3; // 3代表之间间隔
let dialogObj = DialogBus.getDialog(divId);
if (dialogObj == null) {
DialogBus.dialogArray.push({id: divId, obj: curObj, w: width, h: height, isMin: true});
} else {
dialogObj["isMin"] = true;
//DialogBus.dialogLeftCount+=1;
//console.log("最小化了个数:" + DialogBus.dialogLeftCount);
return left;
// 判断是否已经存在
static getDialog(divId) {
let array = DialogBus.dialogArray;
let count = array.length;
for (let index = 0; index < count; index++) {
if (array[index].id == divId) {
// 已经存在了
return array[index];
return null;
* 打开窗口
* @param {*} comp : 组件
static openDialog(comp) {
if (!(undefined != comp.props && comp.props.hasOwnProperty("divId") && undefined != comp.props.divId)) {
Modal.error({
title: TipMsgStore.ModalError,
content: 'divId参数要传入',
return;
let divId = comp.props.divId;
let div = document.getElementById(divId);
if (undefined != div) {
// 已经存在了,并查一下他有没有最小化,如果最小化了,还要把他最大化
let dialog = DialogBus.getDialog(divId);
if (dialog != null) {
// 已经最小化了,那么要把他最大化,并用下面底部要重新排列
// console.log("当前最小化了:"+dialog.isMin);
if (dialog.isMin) {
// 当前最小化了,把他最大化了
dialog.obj.maxDialog(dialog.w, dialog.h);
return;
// 没有最小化,有可能他隐藏在多个窗口后面,这时把他显示到最前面上来, 注意这里的代码永远执行不到,因为现在的机制是点了最小化才会装到那个数组上去,没有最小化的话他数组就没有当前对象
// 除非以后再来定义一个数组
const elem = document.getElementsByClassName('m-dialog');
let curElem;
for (let i=0; i< elem.length; i++) {
curElem = elem[i].parentNode.parentNode.parentNode;
if (curElem.id == divId) {
elem[i].style.zIndex = 99;
} else {
elem[i].style.zIndex = 1;
return;
// 不存在
//console.log("没有存在相同弹出框的ID了");
let newDiv = document.createElement('div');
newDiv.id = divId;
newDiv.className = "myDialog";
ReactDOM.render(comp, document.body.appendChild(newDiv));
* 创建DIV
* @param {* } id
static createDiv(id) {
let newDiv = document.createElement('div');
newDiv.id = id;
return newDiv;
* 重新排序
* @param {*} divId
static sortDialog() {
let dialgArray = DialogBus.dialogArray;
let count = dialgArray.length;
for (let index = 0; index < count; index++) {
dialgArray[index].obj.sortBottom(DialogBus.dialogWidth * index + index * 3);
* 关闭对话框,并把他移除掉
* @param {*} divId
static closeDialog(divId, isRemove = true) {
let dialgArray = DialogBus.dialogArray;
let count = dialgArray.length;
let currObj;
if (isRemove) {
document.body.removeChild(document.getElementById(divId));
for (let index = 0; index < count; index++) {
currObj = dialgArray[index];
if (currObj.id == divId) {
dialgArray.splice(index, 1);
break;
* 从最小化到最大化,底部要重新排列
static minToMaxDialog(divId) {
DialogBus.closeDialog(divId, false); // 把装的最小化数组里面当前对象移掉去
DialogBus.sortDialog(); // 重新排列
最后使用方法:
DialogBus.openDialog(<InformAddView divId={"addinform" + Utils.randomFrom(1,100)} />);
en-us:
Here is an "ant-design-draggable-modal" provided by a foreigner, supporting drag and drop,ant-design-draggable-modal
zh-cn:
这里是一个老外提供的“ant-design-draggable-modal”,支持拖拽;ant-design-draggable-modal
对于7.0.0以后版本很好处理,利用angular官方cdk中的drag组件。记事本分别打开node_modules\ng-zorro-antd\fesm2015和node_modules\ng-zorro-antd\fesm5下的ng-zorro-antd.js文件:
1.查找 ”NzModalModule“,在imports中添加”DragDropModule“
2.查找”ant-modal-wrap“(共找到2个地方,只需修改第2处),在含有”ant-modal-wrap“的
标签加上” cdkDrag“。加好后像这样<div\n (click)="onClickMask($event)"\n class="ant-modal-wrap {{ nzWrapClassName }}"\n [style.zIndex]="nzZIndex"\n [style.display]="hidden ? 'none' : ''"\n tabindex="-1"\n role="dialog"\n cdkDrag >\n
保存后重新运行你的程序就可以了。
当然,你也可直接该原代码文件:
在 nz-modal.module.ts导入DragDropModule
修改nz-modal.component.html,在<div
(click)="onClickMask($event)"
class="ant-modal-wrap {{ nzWrapClassName }}"
[style.zIndex]="nzZIndex"
[style.display]="hidden ? 'none' : ''"
tabindex="-1"
role="dialog" ckdDrag>
中加上cdkDrag 。编译后发布
@chengzequn 不合理,Modal等支持拖拽可以理解,其他的想要支持拖拽怕是为了“UI编辑”这种产品为目的个人诉求吧。
要不考虑一下Modal组件添加几个ref? headRef(ant-modal-header), 这样使玩家自定义拖拽操作更为简单, 我可以直接监听事件且变更css位置样式, 这样子个人都很容易实现拖拽和其他的自定义操作.
我尝试过在title={<div ref={xxx}></div>
}, 但是发现第一次跑useEffect的时候ref.current为null.😁
我使用react-rnd自定义了一个modal组件,可以在npm 上下载使用
npm https://www.npmjs.com/package/react-drag-resize-dialog
GitHub https://github.com/LiuSandy/react-drag-resize-dialog