前言
相比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:
image.png
可以自行決定是否開啟邊界限定!
需要注意的是:在鼠標按下,或者拖拽過程中動態更改鼠標狀態,比如從cursor:default
改為了cursor: move
,可能不會生效,可以使用蒙層的方式替代,比如覆蓋一層透明div,鼠標按下的時候隱藏蒙層達到切換鼠標cursor的目的,這里只是提供一種思路,具體大家可以自行嘗試。
本文原創,若對你有幫助,請點個贊吧,若能打賞不勝感激,謝謝支持!
本文地址:http://www.lxweimin.com/p/f05be231b1fd,轉載請注明出處,謝謝。