UI组件之浮出层

这是2017年百度前端学院的一道题,要求做出一个如图所示的浮出层。

以下是浮出层的html代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="modal">
<div class="modal-dialog" draggable="true">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close">&times;</button>
<h4 class="modal-title">标题</h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="close">取消</button>
</div>
<div draggable="true" class="resizable-handle e-resize"></div>
<div draggable="true" class="resizable-handle n-resize"></div>
<div draggable="true" class="resizable-handle w-resize"></div>
<div draggable="true" class="resizable-handle s-resize"></div>
</div>
</div>
</div>

对外提供了一些可以用来定制浮出层的选项。选项表如下:

选项名称 选项默认值 选项描述
size size:{ width: ‘600px’, height: ‘138px’} 允许自定义浮出层的宽高。
resizeable resizeable: true 允许通过拖拽浮出层的一边来改变大小
dragable dragable: true 允许通过拖拽来改变浮出层的位置
keyboard keyboard: true 当按下 escape 键时关闭浮出层,设置为 false 时则按键无效。
backdrop backdrop: true 指定一个静态的背景,当用户点击模态框外部时不会关闭模态框。
open open: null 指定打开浮出层时的回调函数。
beforeClose beforeClose: null 指定关闭浮出层之前的回调函数。
close close: null 指定关闭浮出层的回调函数。

除了这八个选项,还提供了show和hide两个方法。

方法 描述 实例
show 打开弹出层 let modal=new ModalFunc(options); modal.show();
hide 关闭弹出层 let modal=new ModalFunc(options); modal.hide();

接下来聊一聊我在实现过程中遇到的问题和收获。

1、创建自定义类型

在这里创建了一个ModalFunc的自定义类型,采用的方式是组合使用构造函数与原型模式。基本代码如下。

1
2
3
4
5
6
7
8
9
let ModalFunc = function(elem, opts){
this.element = elem;
this.options = {};
};
ModalFunc.prototype = {
setConfig: function(){},
show:function(){},
hide:function(){}
}

这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。

2、提供定制浮出层的选项

在ModalFunc构造函数中初始化options属性,options的值来源于用户定义或默认值。
如下是在ModalFunc构造函数中初始化options属性的代码。_default_options是options的默认值,opts是用户定义的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//定义options默认值
const _default_options = {
size:{
width: '600px',
height: '138px'
},
resizeable: true,
dragable: true,
//当按下 escape 键时关闭浮出层,设置为 false 时则按键无效。
keyboard: true,
//当用户点击浮出层外部是否会关闭浮出层。
backdrop: true,
// open回调
open: null,
// close回调
close: null,
beforeClose: null
}
this.options = {};
if (opts === undefined) {
this.options = _default_options;
} else {
for (let key in _default_options) {
this.options[key] = opts[key]!==null ? opts[key] : _default_options[key];
}
}

用户是在使用浮出层的页面中new一个ModalFunc的对象,并且传入element和options两个参数。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let modal=new ModalFunc(myModal,{
size:{
width:"500px",
height:'138px'
},
keyboard:true,
resizeable: true,
dragable: true,
backdrop:true,
open: () => {
console.log('浮出层打开');
},
beforeClose: (done) => {
if (confirm('before close')) {
done();
} else {
console.log('close failed by before close callback.');
}
},
close: () => {
console.log('浮出层关闭')
}
});

3、实现keyboard选项

keyboard选项控制当按下 escape 键时关闭浮出层,当设置为 false 则按键无效。
这里我遇上的问题是如何在不人为调整焦点的情况下,当浮出层弹出,焦点自动处于其上。
解决方法是在使用show方法打开浮出层的时候,添加如下代码:

1
2
this.element.setAttribute('tabindex', 0);
this.element.focus();

this.element是指modal元素。这样当打开浮出层,焦点就在浮出层上。

4、实现backdrop选项

backdrop选项控制用户点击浮出层外部时是否关闭浮出层。当设置true则关闭。
这里我遇上的问题是如何实现点击阴影背景可关闭,但点击弹框不可关闭。
解决方法是在modal-dialog元素上设置event.stopPropagation();从而阻止冒泡进一步传播到modal元素上。代码如下:

1
2
3
4
5
6
7
8
9
if(this.options.backdrop){
const dialog = document.getElementsByClassName("modal-dialog");
dialog[0].addEventListener("click",(event)=>{
event.stopPropagation();
},false);
this.element.addEventListener("click",(e)=>{
this.hide();
},false);
}

5、实现dragable选项

插件中的dragable选项控制浮出层是否可以拖拽。想要实现的效果是用鼠标点击浮出层然后在页面内拖动,最终放开鼠标,浮出层就处于放开最后一刻的位置。
在写浮出层的拖拽时,modal-dialog需要定位浮出层在drag过程中的位置,这里使用绝对定位。top和left设置为0,然后使用js使浮出层居中。
这里使用了html5原生的drag和drop事件。
制作可拖动对象非常简单。在要设为可移动的元素上设置 draggable=”true” 属性,再设置dragstart事件监听。

1
2
3
4
this.element.addEventListener('dragstart',(event) => {
dragOffset.x = event.offsetX;
dragOffset.y = event.offsetY;
},false);

在drag事件中只要计算出模态框left和top的值,就能进行定位,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
this.element.addEventListener('drag',(event) => {
event.preventDefault();
if(event.clientX - dragOffset.x<0){
dialog[0].style.left = '0px';
}else if(event.clientY - dragOffset.y<0){
dialog[0].style.top = '0px';
}else{
dialog[0].style.left = (event.clientX - dragOffset.x ) + 'px';
dialog[0].style.top = (event.clientY - dragOffset.y ) + 'px';
}
},false);

在drag事件中有一些和位置有关的属性可以帮助定位:event.clientX和event.clientY,这两个值是当前鼠标焦点到页面最左上角,两个坐标之间的X和Y。除了这两个值,我们还需要鼠标焦点到对话框最左上角的距离,这个值可以通过dragstart事件event.offsetX和event.offsetY获取。如图所示:

在dragenter和dragover事件期间调用preventDefault()方法将指示在该位置允许放置。

1
2
3
4
this.element.addEventListener("dragover", function(event) {
// prevent default to allow drop
event.preventDefault();
}, false);

但是令我困惑的是,未定义dragend事件,浮出层依然可以被放置到正确位置。

6、实现resizeable选项

插件中的resizeable选项实现了横向或纵向拖动浮出层的边框可以改变浮出层的宽度或高度。
实现参考了jquery-ui的缩放功能。
在浮出层的html结构上加了四条透明边框线(为了显示效果加了颜色),如图:

然后分别对这四条边进行drag&drop事件监听。
下面以左边为例,只要计算出当前模态框宽度、left和top值就可以实现左边的拖拽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
w_resize[0].addEventListener('dragstart',(event) => {
dialogWidth = parseInt(dialogContent[0].style.width);
clientX = event.clientX;
dragOffset.x = event.offsetX;
dragOffset.y = event.offsetY;
},false);
w_resize[0].addEventListener('drag',(event) => {
event.stopPropagation();
dialogContent[0].style.width = (dialogWidth - (event.clientX - clientX)) + 'px';
if(event.clientX - dragOffset.x<0){
dialog[0].style.left = '0px';
}else if(event.clientY - dragOffset.y<0){
dialog[0].style.top = '0px';
}else{
dialog[0].style.left = (event.clientX - dragOffset.x ) + 'px';
dialog[0].style.top = (event.clientY - dragOffset.y ) + 'px';
}
},false);
w_resize[0].addEventListener("dragover", function(event) {
// prevent default to allow drop
event.preventDefault();
}, false);