如何在 vue 中实现一个自定义拖拽的指令或插件
前言
在 Vue 中,我们可以通过 自定义指令 或 插件 给任意元素添加拖拽的功能。使用 Vue.directive() 注册全局指令或者使用 Vue.use() 安装插件,这两种方式都可以实现实现动态拖拽的功能。
区别
特性 | Vue.directive() | Vue.use() |
---|---|---|
功能范围 | 直接注册单个全局指令 | 安装一个插件(可包含多个指令、混入等) |
复杂度 | 简单快捷,适合单一功能 | 适合复杂功能或需要配置的场景 |
可配置性 | 无法传递参数 | 支持传递配置对象 |
标准化程度 | 低 | 高(符合 Vue 插件规范) |
实现
下面我们以自定义指令举例,探讨如何实现这个功能
- 步骤 1:创建自定义指令
// draggable.js
const draggable = {inserted(el, binding) {initConfig(el, binding);checkVisibilityAndBind(el);},update(el, binding) {initConfig(el, binding); // 更新配置checkVisibilityAndBind(el);},unbind(el) {cleanup(el);}
};// 初始化/更新配置
function initConfig(el, binding) {let isEnabled = true;let config = {};// 解析指令值类型(应对各种可能的传值场景)if (typeof binding.value === 'boolean') {// 布尔值直接控制启用状态isEnabled = binding.value;} else if (typeof binding.value === 'object' && binding.value !== null) {// 对象配置:合并 isEnabled 和其他参数isEnabled = binding.value.isEnabled !== undefined ? binding.value.isEnabled : true;config = { ...binding.value };} else {// 其他类型(如字符串选择器)默认启用isEnabled = binding.value !== false;config = { handleSelector: binding.value };}// 合并最终配置el._dragConfig = {bounds: {},handleSelector: null,draggingClass: 'dragging',...config,isEnabled // 确保 isEnabled 优先级最高};
}function checkVisibilityAndBind(el) {const { isEnabled } = el._dragConfig;const isVisible = getComputedStyle(el).display !== 'none';if (isVisible && isEnabled && !el._dragHandlers) {bindDrag(el);} else if ((!isVisible || !isEnabled) && el._dragHandlers) {cleanup(el);}
}function bindDrag(el) {const {handleSelector,draggingClass,bounds} = el._dragConfig;let currentX = 0, currentY = 0, startX = 0, startY = 0;const dragHandle = handleSelector ? el.querySelector(handleSelector) : el;if (!dragHandle){return;}const onMouseDown = (e) => {// 检查是否点击在句柄区域const isHandle = handleSelector ? e.target.closest(handleSelector) === dragHandle : true;if (!isHandle){return;}e.preventDefault();// 添加拖拽样式类if (draggingClass){el.classList.add(draggingClass);}// 记录初始位置startX = e.clientX;startY = e.clientY;const style = window.getComputedStyle(el);const matrix = new DOMMatrix(style.transform);currentX = matrix.m41;currentY = matrix.m42;// 拖拽移动处理const onMouseMove = (event) => {const dx = event.clientX - startX;const dy = event.clientY - startY;// 计算新位置(应用边界限制)let newX = currentX + dx;let newY = currentY + dy;if (bounds.minX !== undefined) newX = Math.max(newX, bounds.minX);if (bounds.maxX !== undefined) newX = Math.min(newX, bounds.maxX);if (bounds.minY !== undefined) newY = Math.max(newY, bounds.minY);if (bounds.maxY !== undefined) newY = Math.min(newY, bounds.maxY);el.style.transform = `translate(${newX}px, ${newY}px)`;};// 拖拽结束处理const onMouseUp = () => {// 移除拖拽样式类if (draggingClass){el.classList.remove(draggingClass)}document.removeEventListener('mousemove', onMouseMove);document.removeEventListener('mouseup', onMouseUp);};document.addEventListener('mousemove', onMouseMove);document.addEventListener('mouseup', onMouseUp);};// 保存引用以便清理dragHandle.addEventListener('mousedown', onMouseDown);dragHandle.style.cursor = 'move';el._dragHandlers = { onMouseDown, dragHandle };
}function cleanup(el) {if (el._dragHandlers) {const { dragHandle, onMouseDown } = el._dragHandlers;// 移除事件监听dragHandle.removeEventListener('mousedown', onMouseDown);dragHandle.style.cursor = '';// 确保移除样式类if (el._dragConfig.draggingClass) {el.classList.remove(el._dragConfig.draggingClass);}delete el._dragHandlers;}
}export default draggable;
- 步骤 2:注册指令
// main.js
import Vue from 'vue';
import draggable from './draggable';Vue.directive('draggable', draggable);
- 步骤 3:使用指令
<template><div v-draggable="{handleSelector: '.handle',draggingClass: 'active-drag',bounds: { minX: 0, minY: 0 }}"style="position: absolute; background: #fff; border: 2px solid #ccc;"><!-- 拖拽句柄 --><div class="handle" style="padding: 8px; background: #eee;">[拖拽区]</div><!-- 内容区域 --><div style="padding: 12px;"><p>可自由选中的文字内容...</p><button @click="showPos">获取当前位置</button></div></div>
</template><script>
export default {methods: {showPos() {const el = this.$el.querySelector('[v-draggable]');const style = window.getComputedStyle(el);const matrix = new DOMMatrix(style.transform);alert(`当前位置: X=${matrix.m41}px, Y=${matrix.m42}px`);}}
};
</script><style>
.active-drag {box-shadow: 0 2px 8px rgba(0,0,0,0.15);opacity: 0.9;
}
</style>
关键点和注意事项
- 动态绑定
使用 v-show 控制显示时,通过 checkVisibilityAndBind() 检测元素显示状态,动态绑定/解绑事件。- 位置计算
通过 transform: translate() 实现位移,避免影响布局。记录初始位移值实现累加拖拽。- 事件管理
鼠标按下时绑定移动/松开事件。
元素隐藏或销毁时,自动移除所有相关事件。
更多的使用场景演示
- 场景 1:直接禁用拖拽
<template><div v-draggable="false"> <!-- 完全禁用 -->不可拖拽的内容</div>
</template>
- 场景 2:对象配置动态禁用
<template><div v-draggable="{ isEnabled: allowDrag, handleSelector: '.handle',bounds: { maxX: 500 }}"><div class="handle">拖拽手柄</div><div>内容区域(拖拽状态:{{ allowDrag ? '启用' : '禁用' }})</div></div>
</template><script>
export default {data() {return {allowDrag: true};}
};
</script>
- 场景 3:通过选择器隐式启用
<template><!-- 传递选择器字符串,默认启用拖拽 --><div v-draggable="'.custom-handle'"><div class="custom-handle">自定义拖拽区</div><div>可拖拽内容</div></div>
</template>