青训营项目难点总结—— 图解拖拽核心原理
概述
虽然市面上有很多类似的插件,但是为了研究原理打算还是是自己实现一下,希望通过此次实战可以弄懂实现原理
首先大概先了解一下定位相关的原理,为了方便理解,我花了一些时间做了张图示,标识了关键的位置信息都代表了什么
上图可以很清楚的知道当前元素距离左和上的坐标
样式实现
窗口使用position:fixed
进行定位,通过left,top
控制位置,width
和height
控制元素宽高
拖拽移动js原理
实现步骤:
- 求鼠标点击距离div元素边缘的距离
deltaX
- 鼠标位置 减去
deltaX
求得 div元素距离浏览器左边的距离left
分析:
由于是点击div内任意位置都可以拖动位置,但是div
的坐标是由left
决定的,left
是div边缘到浏览器窗口左边的距离, 所以要先计算出点击事件 距离div
左边的距离,这个距离是鼠标相对div的相对距离,移动中保持不变,然后用e.clientX
减去这个距离,就得出移动之后left
的距离,因为e.clientX
是鼠标当前位置距离浏览器窗口左边的距离。同样的原理可以计算出top
的距离
避免拖动任意元素都移动
判断e.target的class是否是标题栏或者body,如果不是则不进行拖动
问:为什么要给document绑定mousemove而不是当前元素?
因为如果给当前元素绑定mousemove则鼠标只能在当前元素移动会生效,如果鼠标超出当前元素则会失效
核心代码如下:
el.onmousedown = function (e) {
var disx = e.clientX - el.offsetLeft
var disy = e.clientY - el.offsetTop
// console.log(e.target)
// console.log(e.target.className)
const className = e.target.className
if (["window-body body", "bar", "title"].includes(className)) {
document.onmousemove = function (e) {
const end = new Date().getTime()
if (end - start > 100) {
el.style.left = e.clientX - disx + "px"
el.style.top = e.clientY - disy + "px"
}
}
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null
}
}
}
拖拽调整宽高js原理:
样式定义:
设置一个热区用来拖拽,这个热区要超过div元素本身,露出可拖拽的边 并设置透明,所以他的上下左右值都要设置为负值
实现步骤:
- 判断点击的位置(上下左右还是四个角)
- 根据方向更改鼠标样式
- 根据方向结合不同不同的算法,计算上下左右如何调整坐标和宽高
分析原理
判断方向
通过position = getBoundingClientRect
获取div的上下左右边距离浏览器左和上的距离,然后通过event.clientX
获取当前点击位置距离左侧和距离浏览器上部 的距离,通过判断对比这两个值的大小就可以判断出来,比如e.clientX
如果小于position.left
,那么点击的就是左侧,如果e,clientX
大于position.right
那么就是点的右边,以此类推,可以判断出鼠标点击的各个方向,如果是对角,比如左上角,那么就是clientX
要大于left
并且clientY
要小于top
代码如下:
function getDirect(disX, disY, position) {
let direct
let horizenDirect
let verticalDirect
const { left, right, top, bottom } = position
if (disX <= left) {
horizenDirect = "left"
} else if (disX >= right) {
horizenDirect = "right"
}
if (disY <= top) {
verticalDirect = "top"
} else if (disY >= bottom) {
verticalDirect = "bottom"
}
if (verticalDirect == "top" && horizenDirect == "left") {
direct = "left-top"
} else if (verticalDirect === "top" && horizenDirect === "right") {
direct = "right-top"
} else if (verticalDirect === "bottom" && horizenDirect === "left") {
direct = "left-bottom"
} else if (verticalDirect === "bottom" && horizenDirect == "right") {
direct = "right-bottom"
} else if (verticalDirect) {
direct = verticalDirect
} else if (horizenDirect) {
direct = horizenDirect
}
return direct
}
设置鼠标样式
实现步骤:
- 监听
mouseenter
事件 - 通过之前判断的方向,更改鼠标的
cursor
样式
代码如下:
function updateCursor(direct) {
switch (direct) {
case "left":
el.style.cursor = "w-resize"
break
case "right":
el.style.cursor = "e-resize"
break
case "bottom":
el.style.cursor = "s-resize"
break
case "top":
el.style.cursor = "n-resize"
break
case "right-bottom":
el.style.cursor = "se-resize"
break
case "left-bottom":
el.style.cursor = "sw-resize"
break
case "right-top":
el.style.cursor = "ne-resize"
break
case "left-top":
el.style.cursor = "nw-resize"
break
default:
el.style.cursor = "auto"
}
}
el.onmouseenter = function (e) {
e.stopPropagation()
const disX = e.clientX // 获取鼠标按下时光标x的值
const disY = e.clientY // 获取鼠标按下时光标Y的值
const position = win.getBoundingClientRect() //父级元素坐标
const direct = getDirect(disX, disY, position) //获取方向
updateCursor(direct)
}
根据方向动态计算每个方向的横纵坐标和宽高度
分析原理:
就哪从拖动左边举例,首先拖动左边,从左向右拖动,则div的宽度应该减少,并且div的横坐标应该是增加的,此时纵坐标不变,高度也不变化,所以宽度减少的值就是拖动产生的变量deltaX
,由于宽度应该是减少的,所以这个deltaX
应该为负值再加上原本的宽度就是拖拽之后的宽度,而移动的距离恰好也是这个deltaX
,但是由于横坐标是增加的,所以增加的值应该是-deltaX
,最后加上原本的横坐标就是移动之后的横坐标。纵坐标的原理与之类似不再赘述。如果从右边往左拖动,则宽度减少,高度不变,但是div左侧的坐标并没有移动,所以left
不应该变
代码如下:
//宽高限制
const limit = {
w: 200,
h: 200
}
el.onmousedown = function (e) {
e.stopPropagation()
const disX = e.clientX // 获取鼠标按下时光标x的值
const disY = e.clientY // 获取鼠标按下时光标Y的值
const disW = win.offsetWidth // 获取拖拽前div的宽
const disH = win.offsetHeight // 获取拖拽前div的高
const position = win.getBoundingClientRect() //父级元素坐标
const direct = getDirect(disX, disY, position) //获取方向
updateCursor(direct) //更新鼠标指针
document.onmousemove = (d) => {
d.stopPropagation()
let w, h
let x, y
switch (direct) {
case "left":
w = disX - d.clientX + disW
w = w < limit.w ? limit.w : w //宽度限制
h = disH
x = position.left + d.clientX - disX //左侧移动
break
case "right":
w = d.clientX - disX + disW
w = w < limit.w ? limit.w : w //宽度限制
h = disH
break
case "bottom":
w = disW
h = d.clientY - disY + disH
break
case "top":
w = disW
h = disY - d.clientY + disH
h = h < limit.h ? limit.h : h //高度限制
y = position.top + d.clientY - disY
break
case "left-top":
x = position.left + d.clientX - disX
y = position.top + d.clientY - disY
w = disX - d.clientX + disW
h = disY - d.clientY + disH
w = w < limit.w ? limit.w : w //宽度限制
h = h < limit.h ? limit.h : h //高度限制
break
case "left-bottom":
x = position.left + d.clientX - disX //左侧移动
w = disX - d.clientX + disW
h = d.clientY - disY + disH
w = w < limit.w ? limit.w : w //宽度限制
break
case "right-top":
w = d.clientX - disX + disW
h = disY - d.clientY + disH
y = position.top + d.clientY - disY
w = w < limit.w ? limit.w : w //宽度限制
h = h < limit.h ? limit.h : h //高度限制
break
case "right-bottom":
w = d.clientX - disX + disW
h = d.clientY - disY + disH
w = w < limit.w ? limit.w : w //宽度限制
h = h < limit.h ? limit.h : h //高度限制
break
}
win.style.width = w + "px" // 拖拽后物体的宽
win.style.height = h + "px" // 拖拽后物体的高
if (x) {
win.style.left = x + "px"
}
if (y) {
win.style.top = y + "px"
}
}
总结:
我是采用vue指令实现,便于操作dom,使用起来感觉也较为方便,细节还是有优化的空间,比如性能优化,现在感觉拖拽有时候会卡顿,总的来说,通过这次集训的项目实战,对于之前有点模糊的事件位置相关知识点更加清晰了一些,也对拖拽的实现有了更清晰的认识。这算是此次实战的主要收获。
完整代码地址:
参考资料:
blog.csdn.net/bbsyi/artic… www.jb51.net/article/133… www.jianshu.com/p/824eb6f9d…