原生js实现全屏弹窗拖拽

需求

最近做业务开发的时候,遇到一个需求,用户打开弹窗填写内容时,想参照下弹窗空白背景处的内容,但是弹窗挡住了视线,于是用户就想是否可以将弹窗任意拖动。

其实不止这一个场景,在许多的表单编辑器,报表设计器,在线 ps,H5编辑器这样的场景中,拖动是最基础的功能之一。

下面,我们带来一个简单的实验,做一个简单版本的弹窗拖拽功能。

思路

可能你已经想到了,核心方法只有两个:

  1. mousedownmousemovemouseup来监听拖拽事件
  2. 拖拽时根据当前鼠标位置动态改变弹窗的lefttop位置

加上一些细节体验的处理就好了。

代码

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等任何前端框架中。这个案例比较简洁,可以根据自己需求扩展出更贴合业务的功能。

而且当我们写成一个功能的时候,我们要学会进阶思考,比如

  • 如何实现改变弹窗大小?
  • 如何支持移动端拖拽?
  • 是否可以抽离成为通用工具函数?

时间有限,笔者后续有时间再更新自己的学习经验。

评论