【vue3】使用自定义指令,实现el-dialog的拖拽功能。

本文介绍了如何通过自定义指令在Vue3中为Element Plus的el-dialog组件添加拖拽功能。内容包括拖拽功能的实现原理、注意事项、使用方式以及源码分享,并提供了一个在线演示链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实现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.clientXstart.y = e.clientYdialogTitle.style.cursor = 'move' // 改变光标形状}// 鼠标移动,实时跟踪 dialogcontainer.onmousemove = (e: any) => {if (start.x === 0) { // 不是拖拽状态return}move.x = e.clientX - start.xmove.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.xmove.y = e.clientY - start.y// 记录新坐标,作为下次拖拽的初始位置domset.x += move.xdomset.y += move.ydialogTitle.style.cursor = oldCursordialog.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-dialogv-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 这个属性的名称被写死了,不能用其他名称。这是一个偷懒的设定。

源码

gitee.com/naturefw-co…

在线演示

naturefw-code.gitee.io/nf-rollup-u…

最后,最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

### 创建自定义指令实现 `el-dialog` 缩放功能 为了实现Vue 3Element Plus 中创建自定义指令以支持 `el-dialog` 组件的缩放功能,可以遵循以下方法: #### 自定义指令设计思路 通过绑定自定义指令到 `.el-dialog__wrapper` 或者特定于对话框的内容区域,允许用户点击并拖动指定的手柄(通常是四角之一)来改变对话框尺寸。考虑到用户体验和交互逻辑,需确保当用户在特殊 HTML 元素上释放鼠标按钮时仍能正常结束拖拽操作[^1]。 #### 关键点处理 - **样式调整**:移除默认设置中的 `right` 和 `bottom` 属性,仅保留 `left` 和 `top` 来定位对话框的位置。 - **边界检测**:保证用于触发拖拽行为的元素始终位于视口内可见范围之内,防止因超出屏幕而造成不良体验。 - **解决 onmouseup 失效问题**:针对某些情况下无法捕获 `onmouseup` 事件的问题,可以通过监听全局文档级别的 `mouseup` 事件作为替代方案,从而确保即使是在非标准元素处松开鼠标也能正确终止当前的操作流程。 #### 实现代码示例 下面是一个简单的例子展示如何利用 Vue 3 的 Composition API 结合自定义指令完成上述需求: ```javascript import { DirectiveBinding } from &#39;vue&#39;; export default { mounted(el: HTMLElement, binding: DirectiveBinding) { let startX = 0; let startY = 0; let startWidth = 0; let startHeight = 0; const resizeHandler = (e: MouseEvent) => { e.preventDefault(); // 更新宽度高度 el.style.width = `${startWidth + e.clientX - startX}px`; el.style.height = `${startHeight + e.clientY - startY}px`; // 设置最大最小宽高限制 if(parseInt(el.style.width)<200){ el.style.width=&#39;200px&#39; } if(parseInt(el.style.height)<200){ el.style.height=&#39;200px&#39; } // 发布更新后的尺寸给父组件或其他地方使用 binding.value && typeof binding.value === "function" && binding.value({ width: parseInt(el.style.width), height: parseInt(el.style.height)}); }; const mouseUpHandler = () => { document.removeEventListener(&#39;mousemove&#39;, resizeHandler); document.removeEventListener(&#39;mouseup&#39;, mouseUpHandler); }; const handleMouseDown = (event: MouseEvent) => { startX = event.clientX; startY = event.clientY; startWidth = parseFloat(getComputedStyle(el).width.replace(&#39;px&#39;,&#39;&#39;)); startHeight = parseFloat(getComputedStyle(el).height.replace(&#39;px&#39;,&#39;&#39;)); // 添加移动和抬起事件监听器至整个document对象 document.addEventListener(&#39;mousemove&#39;, resizeHandler); document.addEventListener(&#39;mouseup&#39;, mouseUpHandler); // 阻止冒泡影响其他元素 event.stopPropagation(); }; // 将mousedown事件绑定到目标元素上 el.querySelector(&#39;.resize-handle&#39;)?.addEventListener(&#39;mousedown&#39;, handleMouseDown); // 移除原有样式中可能干扰布局的属性 Object.assign(el.style,{ position:&#39;absolute&#39;, right:&#39;unset&#39;,// 只保留left/top两个方向上的绝对定位 bottom:&#39;unset&#39;// 同理取消底部固定 }); }, }; ``` 此段代码展示了如何构建一个能够响应鼠标的按下、移动以及释放动作来自由调节 `el-dialog` 对话框大小的功能模块。需要注意的是实际应用过程中还需要考虑更多细节如动画过渡效果的支持等。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值