编写可拖拽的弹窗
可拖拽的弹窗
在刚刚重构完的项目中使用到了element ui框架,踩了不少坑也学到了不少的东西。其中比较麻烦的是它的dialog弹窗组件是无法移动拖拽的,然而客户又强烈的要求一定要有这个功能,所以就自己写了个可拖拽的弹窗组件。虽然拖拽起来不是很流畅,但是也算是满足要求了。
1. 实现原理
主要的实现原理还是获取鼠标在div中的位置,获取位置后设置div的left和top来达到div跟随鼠标移动的效果。因为写的是vue,所以利用了vue的自定义指令来操作dom。
2. 实现步骤
2-1 设计盒子ui
-
老实说,我经常被吐槽没有审美,设计的样式总是被喷。好在这次是dialog弹窗,网上有大把的参考样式。我大体参考了layer的弹窗做出了一个山寨弹窗。
html代码
<template> <div class="m_showBox" :class="skin"> <div class="showBox_mask"></div> <div class="loading_wrap" v-if="buttonstatus === 1"></div> <div class="pop_box" id="pox-box" v-drag> <p class="pop_box_title"> {{title || "提示"}} <span class="pop_box_close" @click="cancel"></span> </p> <div class="pop_box_content"> <slot></slot> </div> <div class="pop_box_bottom"> <a href="javascript:;" class="cancel_btn" @click="cancel">{{canceltext || "取消"}}</a> <a href="javascript:;" class="confirm_btn" v-if="type === 'confirm'" :class="{widths: buttonstatus === 1}" @click="confirm"> <svg viewBox="25 25 50 50" class="u-circular" v-if="buttonstatus === 1"> <circle cx="50" cy="50" r="20" fill="none" class="path"></circle> </svg> <span :class="{'marginLeft': buttonstatus === 1}">{{confirmtext || '确定'}}</span> </a> </div> </div> </div> </template>
css代码太长放到github上了vueDrag.vue
效果图:
- 设计要点:
1、背景遮罩
我这里选择了使用了3个遮罩板,第一块是覆盖全屏幕的白色遮罩(m_showBox)使用fixed定位,让弹窗的所有内容与浏览器之间不会出现留白。第2块就是上图看到的灰色背景(showBox_mask),用来突显弹窗。最后一块是点击确定的遮罩窗(loading_wrap),来防止提交ajax时,用户点击按钮或修改弹窗数据。
2、弹窗构成
这里的弹窗就包括标题,内容和底部部分。内容部分通过插槽插入内容,底部按钮通过svg来实现提交加载的loading效果。
2-2 定义组件props
属性 | 描述 |
---|---|
skin | 用于控制弹窗的宽度(small, middle, large) |
title | 弹窗标题 |
type | 弹窗类型 |
confirmtext | 确认键文案 |
canceltext | 取消键文案 |
buttonstatus | 控制按钮加载效果 |
通过传入的props值来设置弹窗的样式和文案。
2-3 自定义事件实现按钮回调
-
confirm和cancel自定义事件
定义自定义按钮事件,使用$emit触发。methods: { cancel: function () { this.$emit("cancel"); }, confirm: function () { if (this.buttonstatus === 1) { return; } this.$emit("confirm"); }, },
2-4 自定义指令drag实现拖拽效果
2-4-1 vue的directives。
通过vue自定义指令获取绑定的元素,在对DOM进行操作。关于更多vue自定义指令用法,移步自定义指令
2-4-2 相关属性(事件对象event,dom元素,window对象)。
- event.clientX:clientX事件属性返回当事件被触发时鼠标指针向对于浏览器可视区域的水平坐标。
- event.clientY:clientY事件属性返回当事件被触发时鼠标指针向对于浏览器页面可视区域的垂直坐标。
- offsetLeft/offsetLeftTop属性:可以返回当前元素距离某个定位父辈元素左边与顶部的距离(虽然我的父级遮罩层有了定位,但是它的宽高都是与body保持一致的)。
- offsetWidth/offsetHeight: 返回任何一个元素宽/高度,包括边框和填充
- window.innerHeight/Width: 获取当前页面可视区的宽高(包括滚动条)。
2-4-3 相关事件
事件 | 描述 |
---|---|
onmousedown | 鼠标按钮被按下。 |
onmousemove | 鼠标被移动。 |
onmouseup/td> | 鼠标按键被松开。 |
2-4-4 实现代码
directives: {
drag: {
inserted: function (el, binding, vnode) {
vnode = vnode.elm;
el.onmousedown = ((event) => {
if (event.target.className !== "pop_box_title") {
return;
}
//获取鼠标在盒子中的位置
let mouseX = event.clientX - vnode.offsetLeft;
let mouseY = event.clientY - vnode.offsetTop;
//绑定移动和停止函数
document.onmousemove = ((event) => {
let left, top;
//获取新的鼠标位置对应下的盒子应该在的位置
left = event.clientX - mouseX;
top = event.clientY - mouseY;
//获取div在页面中X轴的最小最大位置
let minX = vnode.offsetWidth / 2;
let maxX = (window.innerWidth - vnode.offsetWidth / 2) - 10//去掉滚动条的宽度
if (left <= minX) {
left = minX;
} else if (left >= maxX) {
left = maxX;
}
//获取div在页面中Y轴的最大最小位置
let minY = vnode.offsetHeight / 2;
let maxY = (window.innerHeight - vnode.offsetHeight / 2);
if (top <= minY) {
top = minY;
} else if (top >= maxY) {
top = maxY;
}
//赋值移动
vnode.style.left = left + 'px';
vnode.style.top = top + 'px';
});
document.onmouseup = (() => {
document.onmousemove = document.onmouseup = null;
});
});
window.onresize = (() =>{
vnode.style.left = "50%";
vnode.style.top = "50%";
});
}
}
}
2-4-5 代码解析
1、给弹窗绑定onmousedown事件,获取到鼠标在弹窗中的位置(以弹窗左上角为原点)。
2、document绑定onmousemove事件,获取当前的鼠标位置,当前鼠标位置减去鼠标在弹窗的相当位置即可得到此时弹窗应该处于的位置。然后在通过style设置弹窗的位置。
3、鼠标松开解绑document的鼠标事件。
注意点:
- 弹窗要一直在页面可视区移动,最大的移动距离就是可视区的宽高减去盒子本身的宽高(还要考虑到浏览器的滚动条的宽高,我的浏览器滚动条是自己设置的,高度为0,宽度为10)。
window.innerHeight - vnode.offsetHeight / 2;
(window.innerWidth - vnode.offsetWidth / 2) - 10; - 只有弹窗标题才能拖拽,所以判断非标题部分之间return。
- 浏览器窗口大小改变会影响弹窗的位置,监听改变浏览器窗口改变把弹窗居中。
2-5 使用
- 下载drag.vue。vueDrag.vue。
-
使用import引入
import vDrag from "./dragDiv.vue"
-
控制弹窗的显示隐藏通过v-if绑定data里的数据即可。
<transition name="el-fade-in"> <v-drag v-if="isShow" :tilte="title" :type="type" @confirm="confirmSubmit" @cancel="cancel" :buttonstatus="buttonstatus"> <el-form label-width="100px"> <el-form-item label="用户名称:"> <el-input placeholder="请输入用户名" v-model="username"></el-input> </el-form-item> <el-form-item label="密码:"> <el-input placeholder="请输入密码" v-model="password"></el-input> </el-form-item> </el-form> </v-drag> </transition>
结语
关于这个组件我觉得还有很多优化的地方,望各位大佬给出意见。