原生js实现全屏弹窗拖拽
需求
最近做业务开发的时候,遇到一个需求,用户打开弹窗填写内容时,想参照下弹窗空白背景处的内容,但是弹窗挡住了视线,于是用户就想是否可以将弹窗任意拖动。
其实不止这一个场景,在许多的表单编辑器,报表设计器,在线 ps,H5编辑器这样的场景中,拖动是最基础的功能之一。
下面,我们带来一个简单的实验,做一个简单版本的弹窗拖拽功能。
思路
可能你已经想到了,核心方法只有两个:
- 用
mousedown
、mousemove
、mouseup
来监听拖拽事件 - 拖拽时根据当前鼠标位置动态改变弹窗的
left
、top
位置
加上一些细节体验的处理就好了。
代码
1. 构造DOM
- 创建一个占满可操控区域的容器
- 容器内部创建一个弹窗,自定义弹窗内容
- 弹窗内的最后位置,放置四个边框,用于挂载拖拽事件,监听鼠标移动
- 最后放置一个弹窗遮罩层,用于在拖拽时遮住弹窗内容,防止拖拽时鼠标 移动太快触发了弹窗内的事件,比如输入、选中事件。
<!--外层容器-->
<div class="container">
<!--内部弹窗-->
<div class="dialog">
<!--弹窗内容-->
弹窗内容
<!--边框-->
<div class="drag-bar-left"></div>
<div class="drag-bar-right"></div>
<div class="drag-bar-top"></div>
<div class="drag-bar-bottom"></div>
<!--弹窗遮罩-->
<div class="mask-drag"></div>
</div>
</div>
2. 调整样式
- 外层容器宽度和高度都设置100%,占满它的外层父级元素即可,特别注意要使用相对定位,这样可以使得内部的弹窗定位生效
- 弹窗需要使用绝对定位来指定位置,不可以使用百分比,因为拖拽的时候需要实时改变弹窗位置,那时候用的就是指定位置
- 内部四个边框的宽度设置为10px,也采用绝对定位固定到弹窗的四周
- 遮罩层先隐藏,等到拖动的时候再遮住弹窗
.container{
width:100%;
height:100%;
position:relative;
}
.dialog{
width:150px;
height:150px;
position:absolute;
left:50px;
top:50px;
background:gray;
}
.drag-bar-left{
background:transparent;
position: absolute;
left:0;
top:0;
bottom:0;
width:10px;
cursor: move;
display: block;
}
.drag-bar-right{
background:transparent;
position: absolute;
right:0;
top:0;
bottom:0;
width:10px;
cursor: move;
display: block;
}
.drag-bar-top{
background:transparent;
position: absolute;
left:0;
top:0;
right:0;
height:10px;
cursor: move;
display: block;
}
.drag-bar-bottom{
background:transparent;
position: absolute;
left:0;
right:0;
bottom:0;
height:10px;
cursor: move;
display: block;
}
/* 拖动时遮住整个弹框放置鼠标触发了里面的按钮 */
.mask-drag{
position: absolute;
cursor: move;
user-select: none;
left:0;
top:0;
}
3. 监听事件
- 监听四个边框
mousedown/mousemove/mouseup
事件 - 在
mousedown
的时候存储鼠标位置距离弹窗左边框的距离diffX = e.clientX - drag.offsetLeft
(上边框同样) - 在
mousemove
的时候根据鼠标位置减去鼠标到左边框的距离,即可实时计算弹窗左边框的位置left = e.clientX - diffX
- 使用遮罩层提升体验
- 处理浏览器兼容性
// 执行
initDragDialog();
/*
* DOM selector
* @param selector {String} css selector for element
* @param context {Element} root element
* usage: $$('head')
* result: You will get <head> element
*/
function $$(selector, context) {
context = context || document
const elements = context.querySelectorAll(selector)
return elements.length == 1
? Array.prototype.slice.call(elements)[0]
: Array.prototype.slice.call(elements)
}
/**
* 弹窗支持拖动
*/
function initDragDialog(){
// 拖拽功能(主要是触发三个事件:onmousedown\onmousemove\onmouseup)
const drag = $$('.dialog');
const divMask = $$('.mask-drag');
const dragBarTop = $$('.drag-bar-top')
const dragBarBottom = $$('.drag-bar-bottom')
const dragBarLeft = $$('.drag-bar-left')
const dragBarRight = $$('.drag-bar-right')
dragBarTop.addEventListener('mousedown',mouseDownLisenter)
dragBarBottom.addEventListener('mousedown',mouseDownLisenter)
dragBarLeft.addEventListener('mousedown',mouseDownLisenter)
dragBarRight.addEventListener('mousedown',mouseDownLisenter)
// 点击某物体时,用drag对象即可,move和up是全局区域,也就是整个文档通用,应该使用document对象而不是drag对象(否则,采用drag对象时物体只能往右方或下方移动)
function mouseDownLisenter (e) {
e = e || window.event // 兼容ie浏览器
const diffX = e.clientX - drag.offsetLeft // 鼠标点击物体那一刻相对于物体左侧边框的距离=点击时的位置相对于浏览器最左边的距离-物体左边框相对于浏览器最左边的距离
const diffY = e.clientY - drag.offsetTop
/* 低版本ie bug:物体被拖出浏览器可视窗口外部时,还会出现滚动条,
解决方法是采用ie浏览器独有的2个方法setCapture()\releaseCapture(),这两个方法,
可以让鼠标滑动到浏览器外部也可以捕获到事件,而我们的bug就是当鼠标移出浏览器的时候,
限制超过的功能就失效了。用这个方法,即可解决这个问题。注:这两个方法用于onmousedown和onmouseup中 */
if (typeof drag.setCapture !== 'undefined') {
drag.setCapture()
}
// 遮住弹窗
divMask.style.width = '100%';
divMask.style.height = '100%';
// 防止拖拽速度过快时触发选中事件
document.body.style.userSelect = 'none'
document.onmousemove = function (e) {
e = e || window.event // 兼容ie浏览器
let left = e.clientX - diffX
let top = e.clientY - diffY
// 1. 控制拖拽物体的范围只能在浏览器视窗内,不允许出现滚动条
// if (left < 0) {
// left = 0
// } else if (left > window.innerWidth - drag.offsetWidth) {
// left = window.innerWidth - drag.offsetWidth
// }
// if (top < 0) {
// top = 0
// } else if (top > window.innerHeight - drag.offsetHeight) {
// top = window.innerHeight - drag.offsetHeight
// }
// 2. 允许拖出界面,但是要保留一部分,防止拉不回来
if (left < -drag.offsetWidth + 100) {
left = -drag.offsetWidth + 100
} else if (left > window.innerWidth - 100) {
left = window.innerWidth - 100
}
if (top < -drag.offsetHeight + 100) {
top = -drag.offsetHeight + 100
} else if (top > window.innerHeight - 100) {
top = window.innerHeight - 100
}
// 移动时重新得到物体的距离,解决拖动时出现晃动的现象
drag.style.left = left + 'px'
drag.style.top = top + 'px'
drag.style.transform = ''
}
document.onmouseup = function (e) { // 当鼠标弹起来的时候不再移动
this.onmousemove = null
this.onmouseup = null // 预防鼠标弹起来后还会循环(即预防鼠标放上去的时候还会移动)
// 修复低版本ie bug
if (typeof drag.releaseCapture !== 'undefined') {
drag.releaseCapture()
}
// 恢复隐藏遮罩层
divMask.style.width = '0';
divMask.style.height = '0';
document.body.style.userSelect = ''
}
}
}
Demo
总结
基本上实现了弹窗拖拽的需求,而且原生js的写法可以快速用在vue/react/angular等任何前端框架中。这个案例比较简洁,可以根据自己需求扩展出更贴合业务的功能。
而且当我们写成一个功能的时候,我们要学会进阶思考,比如
- 如何实现改变弹窗大小?
- 如何支持移动端拖拽?
- 是否可以抽离成为通用工具函数?
时间有限,笔者后续有时间再更新自己的学习经验。
评论