目录
- 实现el-dialog的拖拽功能
- 通过自定义指令实现拖拽功能
- 实现拖拽功能
- 使用方式
实现el-dialog的拖拽功能
这里指的是 element-plus 的el-dialog组件,一开始该组件并没有实现拖拽的功能,当然现在可以通过设置属性的方式实现拖拽。
自带的拖拽功能非常严谨,拖拽时判断是否拖拽出窗口,如果出去了会阻止拖拽。
如果自带的拖拽功能可以满足需求的话,可以跳过本文。
通过自定义指令实现拖拽功能
因为要自己操作dom(设置事件),所以感觉还是使用自定义指令更直接一些,而且对原生组件的影响更小。
我们先定义一个自定义指令 _dialogDrag:
import dialogDrag from './_dialog-drag'
import { watch } from 'vue'
const _dialogDrag = {
// mounted
mounted (el: any, binding: any) {
// 监听 dialog 是否显示的状态
watch (binding.value, () => {
// dialog 不可见,退出
if (!binding.value.visible) return
// 寻找 el-dialog 组件
const container = el.firstElementChild.firstElementChild
// 已经设置拖拽事件,退出
if (container.onmousemove) return
// 等待 DOM 渲染完毕
setTimeout(() => {
// 拖拽的 “句柄”
const _dialogTitle = el.getElementsByClassName('el-dialog__header')
if (_dialogTitle.length === 0) {
// 还没有渲染完毕,或则其他原因
console.warn('没有找到要拖拽的 el-dialog', el)
} else {
const { setDialog } = dialogDrag()
const dialogTitle = _dialogTitle[0]
// 弹窗
const dialog = el.firstElementChild.firstElementChild.firstElementChild
// 通过 css 寻找 el-dialog 设置的宽度
const arr = dialog.style.cssText.split(';')
const width = arr[0].replace('%', '').replace('--el-dialog-width:', '') //
// 设置 el-dialog 组件、弹窗、句柄、宽度
setDialog(container, dialog, dialogTitle, width)
}
},300)
})
},
}
/**
* 注册拖拽 dialog 的自定义指令
* @param app
* @param options
*/
const install = (app: any, options: any) => {
app.directive('dialogDrag', _dialogDrag)
}
export {
_dialogDrag,
install
}
这里有两个比较烦人的地方:
- DOM渲染完毕的时机。执行 mounted 的时候,DOM不一定渲染完毕,如果不使用 setTimeout 的话,就会找不到DOM,所以用了这种笨办法。
- dialog 的隐藏。一般情况下,el-dialog 初始是隐藏状态,隐藏了就意味着DOM并不会被渲染出来。可是自定义指令会在一开始即被执行,这时 setTimeout 的等待时间再长也无用,所以只好监听dialog的状态。
- ref 通过 template 传递后,再次传入组件的话,就会失去ref的那一层的响应性,所以只能传入reactive才行,这样调用指令的组件,就会比较别扭,目前没有想到更好的实现方式。
实现拖拽功能
定义指令和实现拖拽,我分成了两个文件,我想,尽量解耦一下。
定义一个拖拽函数(dialogDrag):
/**
* 拖拽 dialog 的函数,目前支持 element-plus
*/
export default function dialogDrag () {
/**
* 设置拖拽事件
* @param container 大容器,比如蒙版。
* @param dialog 被拖拽的窗口
* @param dialogTitle 拖拽的标题
* @param width 宽度比例
*/
const setDialog = (container: any, dialog: any, dialogTitle: any, width: number) => {
const oldCursor = dialogTitle.style.cursor
// 可视窗口的宽度
const clientWidth = document.documentElement.clientWidth
// 可视窗口的高度
const clientHeight = document.documentElement.clientHeight
// 根据百分数计算宽度
const tmpWidth = clientWidth * (100 - width) / 200
// 默认宽度和高度
const domset = {
x: tmpWidth,
y: clientHeight * 15 / 100 // 根据 15vh 计算
}
// 查看dialog 当前的宽度和高低
if (dialog.style.marginLeft === '') {
dialog.style.marginLeft = domset.x + 'px'
} else {
domset.x = dialog.style.marginLeft.replace('px','') * 1
}
if (dialog.style.marginTop === '') {
dialog.style.marginTop = domset.y + 'px'
} else {
domset.y = dialog.style.marginTop.replace('px','') * 1
}
// 记录拖拽开始的光标坐标,0 表示没有拖拽
const start = { x: 0, y: 0 }
// 移动中记录偏移量
const move = { x: 0, y: 0 }
// 经过时改变鼠标指针形状
dialogTitle.onmouseover = () => {
dialogTitle.style.cursor = 'move' // 改变光标形状
}
// 鼠标按下,开始拖拽
dialogTitle.onmousedown = (e: any) => {
start.x = e.clientX
start.y = e.clientY
dialogTitle.style.cursor = 'move' // 改变光标形状
}
// 鼠标移动,实时跟踪 dialog
container.onmousemove = (e: any) => {
if (start.x === 0) { // 不是拖拽状态
return
}
move.x = e.clientX - start.x
move.y = e.clientY - start.y
// 初始位置 + 拖拽距离
dialog.style.marginLeft = (domset.x + move.x) + 'px'
dialog.style.marginTop = (domset.y + move.y) + 'px'
}
// 鼠标抬起,结束拖拽
container.onmouseup = (e: any) => {
if (start.x === 0) { // 不是拖拽状态
return
}
move.x = e.clientX - start.x
move.y = e.clientY - start.y
// 记录新坐标,作为下次拖拽的初始位置
domset.x += move.x
domset.y += move.y
dialogTitle.style.cursor = oldCursor
dialog.style.marginLeft = domset.x + 'px'
dialog.style.marginTop = domset.y + 'px'
// 结束拖拽
start.x = 0
}
}
return {
setDialog // 设置
}
}
首先观察el-dialog渲染后的DOM结构,发现是通过 marginLeft、marginTop 这两个css 的属性,那么我们的拖拽也可以通过修改这两个属性来实现。
然后就是古老的拖拽思路:按下鼠标的时候,记录光标的初始坐标,抬起鼠标的时候,记录光标的结束坐标,然后计算一下得到x、y的“偏移量”,进而修改 marginLeft、marginTop 这两个属性,即可实现拖拽的效果。
核心思路就是这样,剩下的就是细节完善了。
还有一个小问题,拖拽后关闭,然后再次打开,希望可以在拖拽结束的地方打开,而不是默认的位置。所以又想了个办法记录这个位置。
还是要观察 el-dialog 的行为,最后发现规律,一开始 marginLeft 是空的,而拖拽后会保留位置。所以,判断一下就好。
使用方式
原本想直接给el-dialog 设置自定义指令,但是发现“无效”,所以只好在外面套个div。
<template>
<!--拖拽-->
<el-button @click="dialog.visible = true">打开</el-button>
<div v-dialog-drag="dialog" >
<el-dialog
v-model="dialog.visible"
title="自定义拖拽2"
width="25%"
>
<span>拖拽测试</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialog.visible = false">Cancel</el-button>
<el-button type="primary" @click="dialog.visible = false">Confirm</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from 'vue'
import { _dialogDrag } from '../../../lib/main'
export default defineComponent({
name: 'nf-dialog-move',
directives: {
dialogDrag: _dialogDrag
},
props: {
},
setup(props, context) {
const dialog = reactive({
visible: false
})
return {
meta,
dialog
}
}
})
</script>
如果全局注册了自定义指令,那么组件里面就不用注册了。
dialog 的 visible: visible 这个属性的名称被写死了,不能用其他名称。这是一个偷懒的设定。
源码
在线演示