Elemenut-UI中会发现el-dialog弹框是没有拖拽功能,而在很多项目中是需要这一功能的;在Vue中,可以使用Vue.directive钩子函数,从底层并通过添加标签属性方式,来实现这一功能。
在之前一篇文章中,也写过el-dialog弹框的拖拽功能,因通过position定位方式来实现,所以实现效果不算很好;这一篇,将通过transform动画方式完成弹框拖拽移位,并将其限定在浏览器可见区域的拖拽功能。
一、创建页面
在src/views目录中,创建Dialog/index.vue文件,并在router中添加该页面路径。代码如下:
<template>
<div style="padding: 30px;">
<el-button type="primary" size="mini" @click="() => visible = !visible">显示</el-button>
<el-dialog :visible.sync="visible" width="500px" title="标题">
<div style="min-height: 200px;">这是一个可拖拽的弹框,并且限定在浏览器可限定区域显示。</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
页面效果如下:
二、自定义指令
Vue 2 的自定义指令提供了以下生命周期钩子:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
以下是一个简单的自定义指令示例,用于在元素插入 DOM 后自动获取焦点。
<template>
<div>
<input v-focus />
</div>
</template>
<script>
export default {
directives: {
focus: {
// 当被绑定的元素插入到 DOM 中时…
inserted(el) {
// 聚焦元素
el.focus();
}
}
}
};
</script>
三、全局定义指令
创建src/directives/index.js文件,定义全局自定义指令。示例代码如下:
import Vue from 'vue'
/**
* 定义全局 自定义指令
*/
Vue.directive('dragDialog', {
inserted(el){
// 初始化样式
},
componentUpdated(el){
// 恢复原位置
},
bind(el){
// 定义绑定事件
}
})
此时在main.js引入自定义指令,代码如下:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// 引入指令
import '@/directives/index'
Vue.use(ElementUI);
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
在el-dialog标签上,添加自定义可拖拽的指令。代码如下:
<template>
<div style="padding: 30px;">
<el-button type="primary" size="mini" @click="() => visible = !visible">显示</el-button>
<el-dialog :visible.sync="visible" width="500px" title="标题" v-dragDialog>
<div style="min-height: 200px;">这是一个可拖拽的弹框,并且限定在浏览器可限定区域显示。</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
四、实现拖拽功能
自定义指令可对DOM的操作,实现节点元素的查询、位置的动态调整、事件的定义等。通过自定义指令,可以在 Vue 2 中实现许多强大的功能。
4.1 默认样式
首先,在inserted中查询el-dialog__header的DOM节点元素,在弹框头部样式中设置鼠标样式、内容禁止选择等。
示例代码如下:
Vue.directive('dragDialog', {
inserted(el){
const header = el.querySelector('.el-dialog__header')
// 初始化样式
header.style.cursor = 'pointer' // 鼠标更换为 小手图标
header.style.userSelect = 'none' // 禁止选择内容
},
componentUpdated(el){
// 恢复原位置
},
bind(el){
// 定义绑定事件
}
})
4.2 绑定事件
自定义指令bind只调用一次,所以事件的相关初始化操作,全部在这里进行定义。
首先,使用querySelector()方法查询出弹框(el-dialog)和弹框标题区域(el-dialog)的DOM节点元素,然后通过style的getPropertyValue()属性方法,查询出el-dialog的transform位置信息,并初始化鼠标移动时,所需的position位置信息。
在鼠标按下时,添加document监听事件(移动和放开事件),通过mousemove移动事件,实时计算出弹框的位置数据,并重置transform的x,y轴数据。
当鼠标放开时,移出mousemove和mouseup事件。
代码如下:
import Vue from 'vue'
/**
* 定义全局 自定义指令
*/
Vue.directive('dragDialog', {
inserted(el) {
const header = el.querySelector('.el-dialog__header')
const _dialog = el.querySelector('.el-dialog')
// 初始化样式
header.style.cursor = 'pointer' // 鼠标更换为 小手图标
header.style.userSelect = 'none' // 禁止选择内容
_dialog.style.transform = 'translate(0px, 0px)'
},
bind(el) {
// 查询DOM 和 transform
const _dialog = el.querySelector('.el-dialog')
const _header = el.querySelector('.el-dialog__header')
// 初始化
let _pos = {
left: 0,
top: 0,
x: 0,
y: 0
}
// 设置移动样式
_header.style.cursor = 'move'
// 鼠标移动事件的回调函数
const mousemoveCallback = (e) => {
// 计算位置
let dx = e.clientX - _pos.x
let dy = e.clientY - _pos.y
// 设置 位置
_dialog.style.transform = `translate(${dx}px, ${dy}px)`
}
// 放开鼠标 事件的回调函数
const mouseupCallback = () => {
document.removeEventListener('mousemove', mousemoveCallback)
document.removeEventListener('mouseup', mouseupCallback)
}
// 定义绑定事件
_header.addEventListener('mousedown', (e) => {
// 获取transform
const _transform = _dialog.style.getPropertyValue('transform')
const _matrix = new DOMMatrix(_transform)
// 初始位置
_pos = {
left: _matrix.m41,
top: _matrix.m42,
x: e.clientX,
y: e.clientY
}
// 添加事件
document.addEventListener('mousemove', mousemoveCallback) // 移动事件
document.addEventListener('mouseup', mouseupCallback) // 放开事件
})
// end
}
})
以上功能完成后,弹框则可以灵活的被拖拽到浏览器中任何位置。
4.3 限定范围
如果希望弹框在浏览器可视区域范围内移动,则可以如下图,在mousedown事件点击时,计算出el-dialog弹框四周可移动距离。
代码如下:
import Vue from 'vue'
/**
* 定义全局 自定义指令
*/
Vue.directive('dragDialog', {
inserted(el) {
const header = el.querySelector('.el-dialog__header')
const _dialog = el.querySelector('.el-dialog')
// 初始化样式
header.style.cursor = 'pointer' // 鼠标更换为 小手图标
header.style.userSelect = 'none' // 禁止选择内容
_dialog.style.transform = 'translate(0px, 0px)'
},
bind(el) {
// 查询DOM 和 transform
const _dialog = el.querySelector('.el-dialog')
const _header = el.querySelector('.el-dialog__header')
// 初始化
let _pos = {
left: 0,
top: 0,
x: 0,
y: 0
}
// 设置移动样式
_header.style.cursor = 'move'
// 鼠标移动事件的回调函数
const mousemoveCallback = (e) => {
// 计算位置
let dx = e.clientX - _pos.x
let dy = e.clientY - _pos.y
// 限定左侧位置
if (dx <= -_pos.limitX) {
dx = -_pos.limitX
}
// 限定右侧位置
else if (dx >= _pos.limitX) {
dx = _pos.limitX
}
// 限定顶部位置
if (dy <= -_dialog.offsetTop) {
dy = -_dialog.offsetTop
}
// 限定底部位置
else if (dy >= _pos.limitY) {
dy = _pos.limitY
}
// 设置 位置
_dialog.style.transform = `translate(${dx}px, ${dy}px)`
}
// 放开鼠标 事件的回调函数
const mouseupCallback = () => {
document.removeEventListener('mousemove', mousemoveCallback)
document.removeEventListener('mouseup', mouseupCallback)
}
// 定义绑定事件
_header.addEventListener('mousedown', (e) => {
// 获取transform
const _transform = _dialog.style.getPropertyValue('transform')
const _matrix = new DOMMatrix(_transform)
// 计算边缘
const limitX = (el.offsetWidth - _dialog.offsetWidth) / 2
const limitY= el.offsetHeight - _dialog.offsetTop - _dialog.offsetHeight
// 初始位置
_pos = {
left: _matrix.m41,
top: _matrix.m42,
x: e.clientX,
y: e.clientY,
limitX: Math.abs(limitX),
limitY: Math.abs(limitY)
}
// 添加事件
document.addEventListener('mousemove', mousemoveCallback) // 移动事件
document.addEventListener('mouseup', mouseupCallback) // 放开事件
})
// mousedown end
}
})
此时,再拖拽弹框,则无法拖出浏览器可视区域了。
4.4 恢复位置
但是,如果弹框被关闭后,重新打开,则会发现弹框会在上次移动的位置,而没有恢复到浏览器居中位置。那么,则需要在componentUpdated组件完成更新时,判断transform中x,y位置是否为0。代码如下:
import Vue from 'vue'
/**
* 定义全局 自定义指令
*/
Vue.directive('dragDialog', {
inserted(el) {
const header = el.querySelector('.el-dialog__header')
const _dialog = el.querySelector('.el-dialog')
// 初始化样式
header.style.cursor = 'pointer' // 鼠标更换为 小手图标
header.style.userSelect = 'none' // 禁止选择内容
_dialog.style.transform = 'translate(0px, 0px)'
},
componentUpdated(el) {
// 查询DOM 和 transform
const _dialog = el.querySelector('.el-dialog')
const _transform = _dialog.style.getPropertyValue('transform')
const _matrix = new DOMMatrix(_transform)
// 恢复原位置
if (_matrix.m41 != 0 || _matrix.m42 != 0)
_dialog.style.transform = 'translate(0px, 0px)'
},
bind(el) {
// 略...
}
})
4.5 Bug修复
当el-dialog弹框在初始位置时,transform的x,y轴都为0,所以在计算新位置时,是没有问题的。但是,当弹框被移动后,则重新点击移动,位置会移置初始位置。解决这问题很简单,在计算新位置时,加上x,y轴移动后的位置路径即可。
代码如下:
import Vue from 'vue'
/**
* 定义全局 自定义指令
*/
Vue.directive('dragDialog', {
inserted(el) {
const header = el.querySelector('.el-dialog__header')
const _dialog = el.querySelector('.el-dialog')
// 初始化样式
header.style.cursor = 'pointer' // 鼠标更换为 小手图标
header.style.userSelect = 'none' // 禁止选择内容
_dialog.style.transform = 'translate(0px, 0px)'
},
componentUpdated(el) {
// 查询DOM 和 transform
const _dialog = el.querySelector('.el-dialog')
const _transform = _dialog.style.getPropertyValue('transform')
const _matrix = new DOMMatrix(_transform)
// 恢复原位置
if (_matrix.m41 != 0 || _matrix.m42 != 0)
_dialog.style.transform = 'translate(0px, 0px)'
},
bind(el) {
// 查询DOM 和 transform
const _dialog = el.querySelector('.el-dialog')
const _header = el.querySelector('.el-dialog__header')
// 初始化
let _pos = {
left: 0,
top: 0,
x: 0,
y: 0
}
// 设置移动样式
_header.style.cursor = 'move'
// 鼠标移动事件的回调函数
const mousemoveCallback = (e) => {
// 计算位置
let dx = e.clientX - _pos.x + _pos.left
let dy = e.clientY - _pos.y + _pos.top
// 限定左侧位置
if (dx <= -_pos.limitX) {
dx = -_pos.limitX
}
// 限定右侧位置
else if (dx >= _pos.limitX) {
dx = _pos.limitX
}
// 限定顶部位置
if (dy <= -_dialog.offsetTop) {
dy = -_dialog.offsetTop
}
// 限定底部位置
else if (dy >= _pos.limitY) {
dy = _pos.limitY
}
// 设置 位置
_dialog.style.transform = `translate(${dx}px, ${dy}px)`
}
// 放开鼠标 事件的回调函数
const mouseupCallback = () => {
document.removeEventListener('mousemove', mousemoveCallback)
document.removeEventListener('mouseup', mouseupCallback)
}
// 定义绑定事件
_header.addEventListener('mousedown', (e) => {
// 获取transform
const _transform = _dialog.style.getPropertyValue('transform')
const _matrix = new DOMMatrix(_transform)
// 计算边缘
const limitX = (el.offsetWidth - _dialog.offsetWidth) / 2
const limitY = el.offsetHeight - _dialog.offsetTop - _dialog.offsetHeight
// 初始位置
_pos = {
left: _matrix.m41,
top: _matrix.m42,
x: e.clientX,
y: e.clientY,
limitX: Math.abs(limitX),
limitY: Math.abs(limitY)
}
// 添加事件
document.addEventListener('mousemove', mousemoveCallback) // 移动事件
document.addEventListener('mouseup', mouseupCallback) // 放开事件
})
// mousedown end
}
})
在vue中,增加el-dialog可拖拽功能,就已完成了。