前言
相比github,现在其实更喜欢在博客上记录代码,图文并茂,方便后面使用的时候快速想起来,毕竟写的时候要考虑到小白也能看懂。
回到主题,这种在有定位的盒子内【如:position: relative;】可以拖动其内部盒子【position: absolute;】移动到其他位置的需求其实比较常见,很多时候之前的拖动逻辑换个地方就表现异常了!一点也不复用,搞得每次都要分析一遍哪里减去哪里,哪里的dom获取有问题才正常!这里写个vue3 ts 的通用逻辑,防止以后再写,相同的逻辑写一次就行了嘛,直接一步到位!
支持
1.可选择是否开启边界条件,也就是限定在“有定位父级”范围内!参数openBoundary设置为true即可。
2.可自行处理拖动,传入 moveingCallback 参数即可,注意这是函数,参数为:
3.拖拽结束回调函数:moveEndCallback。
4.拖拽盒子在布局上允许有其他子节点。
5.页面有滚动条不影响拖动。
6.父节点【有定位的父级】和子节点【 position: absolute;】不是直接父子关系也不影响,当然一般不会出现这样的场景。
7.未考虑缩放场景-缩放因子自己结合代码加,只要搞清楚每步获取到的值是真实值还是缩放值就行了,代码有注释,改起来也简单,这里没加是因为懒得改,毕竟这需求不常见。。。
gif 效果演示如下:
代码如下:
divDrag.ts
/**
vue3 div 拖动 通用逻辑
author:yangfeng
date:20231110
*/
import { ref, onMounted, onUnmounted } from 'vue'
/**
* 判断指定dom节点是否是具有定位属性的节点 - 即:position为 absolute | relative | fixed
* @param _node
* @returns {boolean}
*/
function judgeIsLocateNode(_node: HTMLElement) {
let cssStyle = window.getComputedStyle(_node, null)
return cssStyle.position !== 'static' // 不是默认的就是有定位的
}
/**
* 获取指定节点的有定位的父节点
* @param ele 子节点
* @param flag 父节点类或id选择器或者元素节点名称,eg: 类:.app | id: #app | 元素节点名称 body 或者 flag直接是dom对象
* @returns {HTMLElement | null}
*/
function findLocateParentNode(ele: HTMLElement) {
if (!ele) return null;
let parent: HTMLElement | null = ele.parentNode as HTMLElement;
let locateParentNode: HTMLElement | null = null; // 有定位父节点
while (parent && parent.nodeName !== "BODY" && parent.nodeName !== "HTML") {
if (judgeIsLocateNode(parent)) {
// 是定位节点
locateParentNode = parent;
break;
}
parent = parent.parentNode as HTMLElement;
}
// 默认是body
if (!locateParentNode) {
locateParentNode = document.getElementsByTagName("body")[0];
}
return locateParentNode;
}
/**
* div 拖动 通用逻辑
* @param {
moveingCallback, // 当前正在移动回调函数 非必填 - 有此参数则外部自行处理更改定位的逻辑,不传则拖动时更改dragBoxRef的left,top值
moveEndCallback // 移动结束回调函数 非必填
* } param0
* @returns
*/
type funType = (()=>void) | undefined;
type moveingCallbackType = ((e:MouseEvent, arg:{left:number;top:number;})=>void) | undefined
export default function useDivDrag({
moveingCallback, // 当前正在移动回调函数 非必填
moveEndCallback, // 移动结束回调函数 非必填
openBoundary // 是否开启边界条件【将拖拽盒子限制在定位父节点范围内】 - 注意:如果拖拽盒子有margin 偏移或者translate 偏移,会导致看起来不准确
}:{
moveingCallback?: moveingCallbackType;
moveEndCallback?: funType;
openBoundary?: boolean;
}={}) {
const dragBoxRef = ref() // 需要拖动的盒子
const isMoving = ref(false) // 当前是否正在移动
const tools = {
isFunction(fn: any) {
return fn && typeof fn === 'function'
},
getToContainerXY(e: MouseEvent) {
return {
x: e.x || e.pageX,
y: e.y || e.pageY,
}
},
// 添加移除鼠标事件
addRemoveMouseEvent(callback: Function) {
// 鼠标移动
let moveHandle = (moveE: MouseEvent) => {
callback && callback(moveE)
}
// 移除鼠标事件
let clearMouseEvent = () => {
window.removeEventListener('mousemove', moveHandle)
window.removeEventListener('mouseup', clearMouseEvent)
changeMoveing(false)
// 移动结束
if (tools.isFunction(moveEndCallback)){
moveEndCallback && moveEndCallback()
}
}
window.addEventListener('mousemove', moveHandle, false)
window.addEventListener('mouseup', clearMouseEvent, false)
},
}
const changeMoveing = (bool = false) => {
isMoving.value = bool
}
// 鼠标事件监听
const mouseDownEventListenerHandle = (e: MouseEvent) => {
e?.stopPropagation && e.stopPropagation()
e?.preventDefault && e.preventDefault()
changeMoveing(true)
// 1.获取拖拽盒子有定位的父节点距离浏览器的距离
let LocateParentNode = findLocateParentNode(dragBoxRef.value)
let canvasBoxLeft = 0
let canvvasBoxTop = 0
if (LocateParentNode) {
let info = LocateParentNode.getBoundingClientRect()
canvasBoxLeft = info.left
canvvasBoxTop = info.top
}
// 2.被拖拽盒子距离有定位父节点左、上的距离信息
let boxLeft = dragBoxRef.value.offsetLeft
let boxTop = dragBoxRef.value.offsetTop
// 3.鼠标在被拖拽盒子中按下的位置距离信息【距离浏览器】
let { x: mouseLeft, y: mouseTop } = tools.getToContainerXY(e)
// 4.计算出鼠标按下点距离拖拽盒子左侧、顶部的距离 保证后续拖拽时鼠标位置相对拖拽盒子不变
// 若发现拖动有偏移考虑是否是边框引起的
let toBox_X = mouseLeft - boxLeft - canvasBoxLeft // 鼠标距离盒子左侧距离 鼠标距离浏览器左侧距离 - 拖拽盒子距离有定位父节点左侧距离 - 有定位父节点距离左侧距离浏览器左侧距离
let toBox_Y = mouseTop - boxTop - canvvasBoxTop // 鼠标距离盒子顶部距离
tools.addRemoveMouseEvent((moveE: MouseEvent) => {
let { x, y } = tools.getToContainerXY(moveE) // 鼠标点击位置,距离画布边界的距离
let left = x - toBox_X - canvasBoxLeft
let top = y - toBox_Y - canvvasBoxTop
let dragDom = dragBoxRef.value
// 拖拽边界 拖拽盒子不允许超出定位父节点
if(openBoundary){
try {
let minX = 0;
let minY = 0
let maxX = LocateParentNode!.clientWidth - dragDom.offsetWidth;
let maxY = LocateParentNode!.clientHeight - dragDom.offsetHeight;
left<minX && (left = minX)
top<minY && (top = minY)
left>maxX && maxX>0 && (left = maxX)
top>maxY && maxY>0 && (top = maxY)
} catch (error) {}
}
if (tools.isFunction(moveingCallback)) {
// 有回调函数,交给外部处理
moveingCallback && moveingCallback(
moveE, // 鼠标event
{
left, // 拖动盒子现在的left 像素
top, // 拖动盒子现在的top 像素
},
)
} else {
// 没有回调函数,直接更改
dragDom.style.left = left + 'px'
dragDom.style.top = top + 'px'
}
})
}
onMounted(() => {
if (!dragBoxRef.value) return console.error('dragBoxRef 未绑定到需要移动的 dom 上!')
dragBoxRef.value.addEventListener('mousedown', mouseDownEventListenerHandle, false)
})
onUnmounted(() => {
dragBoxRef.value &&
dragBoxRef.value.removeEventListener('mousedown', mouseDownEventListenerHandle)
})
return {
dragBoxRef, // 需要拖动的盒子 ref
isMoving, // 当前是否正在移动
}
}
测试demo如下:
<!-- 盒子拖拽测试demo -->
<template>
<div class="wrap">
<!-- demo1 -->
<p>基础demo <span class="red-span" v-show="isMoving_demo1">正在移动...</span></p>
<div class="demoBox">
<div :class="{ 'dragBox': true, 'move': isMoving_demo1 }" ref="dragBoxRef_demo1">
移动盒子
</div>
</div>
<!-- demo2 -->
<p>拖动区域限定在边界范围内 <span class="red-span" v-show="isMoving_demo2">正在移动...</span></p>
<div class="demoBox">
<div :class="{ 'dragBox': true, 'move': isMoving_demo2 }" ref="dragBoxRef_demo2">
移动盒子
</div>
</div>
<!-- demo3 -->
<p>使用 moveingCallback 自行处理拖动 <span class="red-span" v-show="isMoving_demo3">正在移动...</span></p>
<div class="demoBox">
<div :class="{ 'dragBox': true, 'move': isMoving_demo3 }" ref="dragBoxRef_demo3">
移动盒子
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import useDivDrag from './divDrag'
// demo1
const {
dragBoxRef: dragBoxRef_demo1, // 需要拖动的盒子 ref
isMoving: isMoving_demo1, // 当前是否正在移动
} = useDivDrag()
// demo2
const {
dragBoxRef: dragBoxRef_demo2, // 需要拖动的盒子 ref
isMoving: isMoving_demo2, // 当前是否正在移动
} = useDivDrag({
openBoundary: true // 开启边界条件【将拖拽盒子限制在定位父节点范围内】
})
// demo3
const {
dragBoxRef: dragBoxRef_demo3, // 需要拖动的盒子 ref
isMoving: isMoving_demo3, // 当前是否正在移动
} = useDivDrag({
// openBoundary: true, // 开启边界条件【将拖拽盒子限制在定位父节点范围内】
moveEndCallback: () => {
console.log('拖动结束!')
},
moveingCallback: (e, { left, top }) => {
dragBoxRef_demo3.value.style.left = left + 'px'
dragBoxRef_demo3.value.style.top = top + 'px'
}
})
</script>
<style scoped lang="scss">
p {
font-weight: bold;
}
.red-span {
margin-left: 10px;
color: red;
text-shadow: 4px 4px 10px #000000;
font-weight: normal;
}
.demoBox {
width: 500px;
height: 300px;
margin: 20px auto;
border: 1px solid #000000;
box-sizing: border-box;
position: relative;
}
.dragBox {
width: 80px;
height: 60px;
border: 1px solid #dddddd;
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
cursor: move;
display: flex;
justify-content: center;
align-items: center;
&.move {
box-shadow: rgb(1, 10, 21) 0px 0px 16px;
z-index: 9;
}
}
</style>
效果为,也就是上面的gif:
可以自行决定是否开启边界限定!
需要注意的是:在鼠标按下,或者拖拽过程中动态更改鼠标状态,比如从cursor:default
改为了cursor: move
,可能不会生效,可以使用蒙层的方式替代,比如覆盖一层透明div,鼠标按下的时候隐藏蒙层达到切换鼠标cursor的目的,这里只是提供一种思路,具体大家可以自行尝试。
本文原创,若对你有帮助,请点个赞吧,若能打赏不胜感激,谢谢支持!
本文地址:https://www.jianshu.com/p/f05be231b1fd,转载请注明出处,谢谢。