实现功能很简单,就是nzModal像windows里的窗口一样,可以拖拽窗口以移动窗口位置。
思路也很简单。创建一个Directive,在Directive中先获取到modal-title的ElementRef对象和nz-modal的ElementRef。然后通过Render2为 modal-title 添加鼠标事件,再通过Render2修改nz-modal的style属性,达到跟随鼠标拖拽而移动的效果。
先创建一个新的Directive,并且引入ElementRef和Render2
import { Directive,ElementRef,Renderer2,AfterViewInit } from '@angular/core';
@Directive({
selector: '[zmMovableModal]'
})
export class ZmMovableModalDirective {
constructor(private elementRef:ElementRef,private render:Renderer2) {
}
ngAfterViewInit(){
}
}
在你要事件拖拽的nzModal中增加这个新的Directive
<nz-modal zmMovableModal nzTitle="Modal1" nzMask="false" (nzOnCancel)="hideModals()" [nzVisible]="modal1Visible"></nz-modal>
获取Element
首先我们在ZmMovableModalDirective中添加获取Title 和 nzModal的方法:
getModalElement(){
return this.elementRef.nativeElement.querySelector('.ant-modal');
}
getModalTitleElment(){
return this.elementRef.nativeElement.querySelector('.ant-modal-header');
}
在ngAfterViewInit()里执行。
ngAfterViewInit(){
let modalElement = this.getModalElement();
let modalTitleElement = this.getModalTitleElment();
console.log(modalElement)
console.log(modalTitleElement);
}
如果控制台成功的打印出了对应的html代码段,就代表成功获取到了title和modal
如果是空的,可能是nzModal版本升级导致类名发生了变动,可以确定querySelector函数对应的class是否正确。
参数准备
在获取到title 和modal 后,我们先为实现拖拽功能准备一些参数:
//只有当鼠标点下之后,鼠标抬起之前,才能移动
private canMove :boolean = false;
//modal开始移动时的x,y坐标
private modalX : number = 0;
private modalY : number = 0;
//鼠标点下时,鼠标所在的坐标
private mouseDownX :number = 0;
private mouseDownY :number = 0;
有了这些准备后,我们开始为title添加鼠标事件。
鼠标事件
首先是鼠标点下的事件:
this.render.listen(modalTitleElement,'mousedown',function(event){
//记录modal和鼠标点击的起始坐标
this.mouseDownX = event.clientX;
this.mouseDownY = event.clientY;
//offsetLeft和offsetTop是相对窗口的坐标。
//我们的modal的移动正好也是基于整个窗口的。所以实现起来很简单。
this.modalX = modalElement.offsetLeft;
this.modalY = modalElement.offsetTop;
//将modal改为绝对定位。并且根据现在窗口的位置设置left和top
this.render.setStyle(modalElement,"position","absolute");
this.render.setStyle(modalElement,"top",`${this.modalY}px`);
this.render.setStyle(modalElement,"left",`${this.modalX}px`);
//一切都准备好了 将状态设置 可移动。
this.canMove = true;
}.bind(this));
然后设置mousemove的事件:
this.render.listen(modalTitleElement,'mousemove',function(event){
//只有在camMove状态下,才进行处理
if(this.canMove){
//获得当前鼠标位置,并根据开始拖拽时的位置,计算出鼠标的偏移量
let moveX = event.clientX - this.mouseDownX;
let moveY = event.clientY - this.mouseDownY;
//鼠标的偏移量就是modal的偏移量。
//通过开始是modal的位置和偏移量计算Modal新的坐标
let newModalX = this.modalX + moveX;
let newModalY = this.modalY + moveY;
//将新坐标设置下去,这样modal就跟着鼠标移动了。
this.render.setStyle(modalElement,"top",`${newModalY}px`);
this.render.setStyle(modalElement,"left",`${newModalX}px`);
}
}.bind(this));
最后,是出挑抬起后的处理:
this.render.listen(modalTitleElement,'mouseup',function(event){
//将状态设置为不可移动
this.canMove = false;
}.bind(this));
这样nzModal拖拽的功能就实现了。
快速拖拽时产生BUG处理
虽然整体的功能实现了,但是还是存在一个问题。
在快速上下拖拽的时候,会有跟丢的现象。
应该是因为快速的鼠标飞出了title 的范围,那么加载在title上的mousemove事件就不会被调用,modal就不会移动了。
所以将mousemove事件绑定到更大的范围就处理了,
所以将
this.render.listen(modalTitleElement,'mousemove',function(event){
........
改成:
this.render.listen(this.elementRef.nativeElement,'mousemove',function(event){
............
无论怎么摇晃鼠标都不会跟丢了。
整体代码:
import { Directive,ElementRef,Renderer2,AfterViewInit } from '@angular/core';
@Directive({
selector: '[zmMovableModal]'
})
export class ZmMovableModalDirective {
private canMove :boolean = false;
private modalX : number = 0;
private modalY : number = 0;
private mouseDownX :number = 0;
private mouseDownY :number = 0;
constructor(private elementRef:ElementRef,private render:Renderer2) {
}
ngAfterViewInit(){
let modalElement = this.getModalElement();
let modalTitleElement = this.getModalTitleElment();
this.render.listen(modalTitleElement,'mousedown',function(event){
this.mouseDownX = event.clientX;
this.mouseDownY = event.clientY;
this.modalX = modalElement.offsetLeft;
this.modalY = modalElement.offsetTop;
this.render.setStyle(modalElement,"position","absolute");
this.render.setStyle(modalElement,"top",`${this.modalY}px`);
this.render.setStyle(modalElement,"left",`${this.modalX}px`);
this.canMove = true;
}.bind(this));
this.render.listen(modalTitleElement,'mouseup',function(event){
this.canMove = false;
}.bind(this));
this.render.listen(this.elementRef.nativeElement,'mousemove',function(event){
if(this.canMove){
let moveX = event.clientX - this.mouseDownX;
let moveY = event.clientY - this.mouseDownY;
let newModalX = this.modalX + moveX;
let newModalY = this.modalY + moveY;
this.render.setStyle(modalElement,"top",`${newModalY}px`);
this.render.setStyle(modalElement,"left",`${newModalX}px`);
}
}.bind(this));
}
getModalElement(){
return this.elementRef.nativeElement.querySelector('.ant-modal');
}
getModalTitleElment(){
return this.elementRef.nativeElement.querySelector('.ant-modal-header');
}
}
新的问题
最近在使用中发现一个问题,就是如果nzTitle 是一个变量,那么虽然最后生成的html代码中有ant-modal-title这个div标签。但是在elementRef.nativeElement里却没相应的element。
这里还没找到原因。
想办先想办法绕过这个问题。
- 1 将整个ant-modal-content作为拖拽的句柄。这个element肯定有的。
但是有一个问题。如果modal中有文字需要复制的时候,你就无法通过鼠标选中文字进行复制。 - 2 如果发现没有ant-modal-header,那么,向里面加入一个新的小element,作为拖拽的句柄。
这样做的问题是操作不一致。会让用户产生疑惑。
最好还是能找到ant-modal-header element的根本原因是什么,从根本上解决问题。
将getModalTitleElement()方法代码改成如下代码,解决了这个问题:
getModalTitleElment(){
// let header = this.elementRef.nativeElement.querySelector('.ant-modal-header');
let element = document.createElement("div") as any;
// this.render.setStyle(element,"background","orange");
this.render.setStyle(element,"width","100%");
this.render.setStyle(element,"height","60px");
this.render.setStyle(element,"position","absolute");
this.render.setStyle(element,"top","0");
this.render.setStyle(element,"left","0");
this.render.appendChild(this.modalElement,element);
return element;
}
这样,只要用户拖动最上方的一段都可以拖拽整个模态框了。
使用RxJS
最近深入学习一些RxJS的用法。正好拿这个指令练练手。
获取element的方法不用改变。
在ngAfterViewInit中
let modalElement = this.getModalElement();
tihs.modalElement = modalElement;
let modalTitleElement = this.getModalTitleElement();
const mouseDown = Observable.fromEvent<MouseEvent>(modalTitleElement,"mousedown");
const mouseUp = Observable.fromEvent<MouseEvent>(this.elementRef.nativeElement,"mouseup");
const mouseMove = Observable.fromEvent<MouseEvent>(this.elementRef.nativeElement,"mousemove");
mouseDown.subscrible(event=>{
this.mouseDownX = event.clentX;
this.mouseDownY = event.clentY;
this.modalX = modalElement.offsetLeft;
this.modalY = modalElement.offsetTop;
this.render.setStyle(modalElement,"position","absolute");
this.render.setStyle(modalElement,"top",`${this.modalY}px`);
this.render.setStyle(modalElelment,"left",`${this.mdoalX}px`);
});
mouseDown.map(event=>mouseMove.takeUntil(mouseUp))
.concatAll()
.subscribe(e=>{
let moveX = e.clientX - this.mouseDownX;
let moveY = e.clientY - this.mouseDownY;
let newModalX = this.modalX + moveX;
let newModalY = this.modalY + moveY;
this.render.setStyle(modalElement,"top",`${newModalY}px`);
this.render.setStyle(modalElement,"left",`${newModalX}px`);
});
不知道有什么方法可以用一串RxJS代码就实现整个功能。未来想到办法会更新上去。
新的解决方案
将上面代码的两个RxJS代码块合并为一个:
mouseDown.map(event=>{
this.modalX = modalElement.offsetLeft;
this.modalY = modalElement.offsetTop;
this.render.setStyle(modalElment,"position","absolute");
this.render.setStyle(modalElement,"top",`${this.modalY}px`);
this.render.setStyle(modalElement,"left",`$(this.modalX)px`);
}).map(event=>mouseMove.takeUntil(mouseUp))
.contactAll()
.withLatestFrom(mouseDown,(move,down)=>{
return {x:move.clientX-down.clientX,y:move.clientY-down.clientY}
})
.subscribe(pos=>{
this.render.setStyle(modalElement,"top",`${this.modalY+pos.y}px`);
this.render.setStyle(modalElement,"left",`$(this.modalX+pos.x)px`);
})