前言
最近在接到一个需求,需要在一个div上实现一个可以拖拽拉伸的组件,老实说之前没怎么写过这种类型的组件,正好借这个机会写一个小轮子,自娱自乐一下。
组件介绍
这个组件是一个支持拖拽移动,支持通过拉拽边框变化大小的组件。
组件采用绝对布局去定位,因此其父节点需要设置为相对布局,可以用于页面侧边的拖拽小组件。
效果如下:
注:值得注意的是组件的大小并不由内容区撑开,而是由组件自身决定,因此使用时需要注意
类型设计
在开发之前,我们需要稍微设计一下组件的类型,类型如下:
ts
// 安全距离
export type SAFE_DISTANCE = {
top?: number
left?: number
bottom?: number
right?: number
}
// 容器开始位置
export type POSITION = {
top?: string | number
left?: string | number
bottom?: string | number
right?: string | number
}
// 容器类型
export type DragBoxType = {
parentDom: HTMLElement // 父节点
minHeight: string | number // 最小高度
maxHeight: string | number // 最大高度
minWidth: string | number // 最小宽度
maxWidth: string | number // 最大宽度
canDrag?: boolean // 是否支持拖拽
position?: POSITION // 起始位置
safeDistance?: SAFE_DISTANCE // 安全距离
horizontalMove?: boolean // 是否支持横向拖动
verticalMove?: boolean // 是否支持垂直拖动
disableUserSelect?: boolean // 拖动或拉拽边框时是否禁用默认用户选中
[DARG_LINE.B_LINE]?: boolean // 是否支持拖拽底部边框
[DARG_LINE.L_LINE]?: boolean // 是否支持拖拽左边边框
[DARG_LINE.T_LINE]?: boolean // 是否支持拖拽顶部边框
[DARG_LINE.R_LINE]?: boolean // 是否支持拖拽右边边框
}
实现原理
原理比较简单主要分为两个部分:拖拽容器移动,拖拽容器边框改变边框尺寸
一、html 结构
讲述下面的实现原理之前我们展示组件的html结构
html
<template>
<div v-if="visible" ref="dragBox" class="main" :style="{ ...basicConfig.position }">
<div :ref="DARG_LINE.T_LINE" class="resize-y border-t"></div>
<div class="g-flex">
<div :ref="DARG_LINE.L_LINE" class="resize-x border-l"></div>
<div ref="dragBoxContent" :style="{ width: width + 'px', height: height + 'px' }">
<slot name="content"></slot>
</div>
<div :ref="DARG_LINE.R_LINE" class="resize-x border-r"></div>
</div>
<div :ref="DARG_LINE.B_LINE" class="resize-y border-b"></div>
</div>
</template>
这里定义了很多的ref,其中包括 dragBox(拖拽容器)
,dragBoxContent(拖拽容器内容区)
,DARG_LINE.T_LINE(顶部拉拽边框)
,DARG_LINE.B_LINE(底部拉拽边框)
,DARG_LINE.R_LINE(右边拉拽边框
),DARG_LINE.L_LINE(左边拉拽边框)
中间还有一个插槽用于存放拖拽容器内容 <slot name="content"></slot>
上述便是拖拽容器组件的基本结构,后续的拖拽功能都是基于上述ref的事件实现。
二、拖拽容器移动
这一步主要是用于拉拽容器进行位移,这里涉及到dragBoxContent
以及 传入的 parentDom
。
初始化
定义一个拖拽初始化函数dragInit
,在组件初始化的时候调用,这个函数的功能主要是为目标元素绑定拖拽相关事件,包括鼠标按下,移动及抬起等,同时定义一些局部变量 startX, startY(鼠标起始的坐标)
,initialLeft, initialTop(dragBoxContent的起始坐标)
。
js
dragInit() {
const parentDom = this.basicConfig.parentDom
const dragBoxContent = (this.$refs.dragBoxContent as HTMLElement)
const dragBox = (this.$refs.dragBox as HTMLElement)
let startX, startY, initialLeft, initialTop
// 鼠标按下,直接监听按下事件
// 鼠标移动
// 鼠标抬起
}
监听鼠标按下
当拖拽容器的内容区按下时,就会触发鼠标按下的回调事件:
js
dragBoxContent.addEventListener('mousedown', (event) => {
if (!this.basicConfig.canDrag) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
startX = event.clientX
startY = event.clientY
const { left, top } = dragBoxContent.getBoundingClientRect()
initialLeft = left
initialTop = top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
-
首先检查是否允许拖拽操作。
-
接下来,检查是否禁用了用户选择(disableUserSelect)。如果禁用了,先保存当前的用户选择样式(document.body.style.userSelect)到变量this.disableUserSelect中,并将文档的用户选择样式设置为'none',保证拖拽过程中用户无法选择文本。
-
然后,记录
鼠标按下时的坐标(event.clientX和event.clientY),作为拖拽操作的起始点。
-
接下来,
获取拖拽框的当前位置(left和top)使用getBoundingClientRect()方法,并将其记录为初始位置(initialLeft和initialTop)。用于后续计算新位置。
-
最后,它向文档添加了两个事件监听器:mousemove和mouseup。mousemove事件监听器绑定了名为onMouseMove的函数,用于处理鼠标移动事件。mouseup事件监听器绑定了名为onMouseUp的函数,用于处理鼠标松开事件。
监听鼠标移动
鼠标按下后会为节点绑定鼠标移动事件,拖拽功能就是在这里去实时更新位置的。
js
const onMouseMove = (event) => {
// 获取偏移量
const offsetX = event.clientX - startX
const offsetY = event.clientY - startY
// 获取新的四边的坐标,用于更新位置和判断是否超出边界
const newLeft = initialLeft + offsetX
const newTop = initialTop + offsetY
const newRight = newLeft + dragBox.getBoundingClientRect().width
const newBottom = newTop + dragBox.getBoundingClientRect().height
// 判断是否支持移动及更新坐标,下同
if (newLeft >= (parentDom.getBoundingClientRect().left + (this.basicConfig.safeDistance.left || 0)) && newRight <= (parentDom.getBoundingClientRect().right - (this.basicConfig.safeDistance.right || 0)) && this.basicConfig.horizontalMove) {
dragBox.style.left = `${newLeft - parentDom.getBoundingClientRect().left}px`
}
if (newTop >= (parentDom.getBoundingClientRect().top + (this.basicConfig.safeDistance.top || 0)) && newBottom <= (parentDom.getBoundingClientRect().bottom - (this.basicConfig.safeDistance.bottom || 0)) && this.basicConfig.verticalMove) {
dragBox.style.top = `${newTop - parentDom.getBoundingClientRect().top}px`
}
}
-
首先,用当前鼠标位置(
event.clientX
和event.clientY
)减去拖拽操作开始时的位置(startX
和startY
),得到鼠标在X轴和Y轴上的偏移量(offsetX
和offsetY
)。 -
接下来,根据初始位置(
initialLeft
和initialTop
)和偏移量(offsetX
和offsetY
)计算出拖拽框的新位置。新的左边界(
newLeft
)等于初始左边界加上X轴偏移量,新的顶部边界(newTop
)等于初始顶部边界加上Y轴偏移量。根据新位置计算出容器的新右边界(
newRight
)和新底部边界(newBottom
),再使用getBoundingClientRect()
方法获取dragBox
的宽度和高度,并将其加到新的左边界和顶部边界上,得到新的右边界和底部边界。这样就可以获得四边坐标(
更新坐标只需要newTop和newLeft就行,拿四边坐标主要是为了做边界判断
)。 -
进行边界判断。
(1)新的左边界是否大于等于父元素(
parentDom
)的左边界加上安全距离(如果没有指定安全距离,则默认为0)。(2)新的右边界是否小于等于父元素的右边界减去安全距离(如果没有指定安全距离,则默认为0)
(3)
this.basicConfig.horizontalMove
是否为真。如果满足这些条件,表示拖拽框可以在水平方向上移动,那么函数会根据新的左边界和父元素的左边界之差,将拖拽框的左边距样式(
dragBox.style.left
)设置为相应的值,下同 -
类似地,函数还检查新的顶部边界是否大于等于父元素的顶部边界加上安全距离,新的底部边界是否小于等于父元素的底部边界减去安全距离,以及
this.basicConfig.verticalMove
是否为真。如果满足这些条件,表示拖拽框可以在垂直方向上移动,那么函数会根据新的顶部边界和父元素的顶部边界之差,将拖拽框的顶部边距样式(dragBox.style.top
)设置为相应的值。
监听鼠标抬起
这一步比较简单,只要把监听事件移除就行,值得注意的是在上面我们加了useSelect,所以要回写回去。
js
const onMouseUp = (_event) => {
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
完整拖拽移动函数
js
dragInit () {
const parentDom = this.basicConfig.parentDom
const dragBoxContent = (this.$refs.dragBoxContent as HTMLElement)
const dragBox = (this.$refs.dragBox as HTMLElement)
let startX, startY, initialLeft, initialTop
dragBoxContent.addEventListener('mousedown', (event) => {
if (!this.basicConfig.canDrag) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
startX = event.clientX
startY = event.clientY
const { left, top } = dragBoxContent.getBoundingClientRect()
initialLeft = left
initialTop = top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
const onMouseMove = (event) => {
const offsetX = event.clientX - startX
const offsetY = event.clientY - startY
const newLeft = initialLeft + offsetX
const newTop = initialTop + offsetY
const newRight = newLeft + dragBox.getBoundingClientRect().width
const newBottom = newTop + dragBox.getBoundingClientRect().height
if (newLeft >= (parentDom.getBoundingClientRect().left + (this.basicConfig.safeDistance.left || 0)) && newRight <= (parentDom.getBoundingClientRect().right - (this.basicConfig.safeDistance.right || 0)) && this.basicConfig.horizontalMove) {
dragBox.style.left = `${newLeft - parentDom.getBoundingClientRect().left}px`
}
if (newTop >= (parentDom.getBoundingClientRect().top + (this.basicConfig.safeDistance.top || 0)) && newBottom <= (parentDom.getBoundingClientRect().bottom - (this.basicConfig.safeDistance.bottom || 0)) && this.basicConfig.verticalMove) {
dragBox.style.top = `${newTop - parentDom.getBoundingClientRect().top}px`
}
}
const onMouseUp = (_event) => {
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
},
三、监听容器边框拖拽,更新容器尺寸
这一步主要是用于拉拽容器边框,进而更新容器尺寸,使得容器变大变小,主要涉及DARG_LINE.T_LINE(顶部拉拽边框)
,DARG_LINE.B_LINE(底部拉拽边框)
,DARG_LINE.R_LINE(右边拉拽边框
),DARG_LINE.L_LINE(左边拉拽边框)
这四个边框。
初始化
处理容器边框拖拽的时候,我们也需要执行初始化函数,但是相对于拖拽只监听容器内容节点而言,边框的监听会多一些。
先定义一个 resizeLineInit
,用于遍历触发各个边框的的初始化。
遍历触发初始化函数:
js
//RESIZE_LINE_ARR: [DARG_LINE.B_LINE, DARG_LINE.L_LINE, DARG_LINE.R_LINE, DARG_LINE.T_LINE],
resizeLineInit () {
const dragBox = (this.$refs.dragBox as HTMLElement)
this.RESIZE_LINE_ARR.forEach(item => {
this.lineInit(this.$refs[item] as HTMLElement, dragBox, item)
})
}
遍历的过程中对每一个边框调用lineInit
进行真实初始化,这一步和上面拖拽的dragInit
很像。
这个函数的功能主要是为目标元素绑定拖拽相关事件,包括鼠标按下,移动及抬起等,同时定义一些局部变量 width(拖拽容器宽)
, height(拖拽容器高)
, left(横坐标)
, top(纵坐标)
, startCoord(起始计算位置)
, isResizing(是否正在更新尺寸)
, parentDom (暂存父节点)
, isVertical(是否操作垂直方向)
。
初始化函数:
js
// line:边框节点,dragBox:拖拽容器节点,lineKey:边框对应的key
lineInit (line: HTMLElement, dragBox: HTMLElement, lineKey:DARG_LINE) {
let width, height, left, top
// 起始位置
let startCoord = 0
let isResizing = false
// 父节点
const parentDom = this.basicConfig.parentDom
// 是否操作垂直方向
const isVertical = [DARG_LINE.T_LINE, DARG_LINE.B_LINE].includes(lineKey)
// 鼠标按下,直接监听按下事件
// 鼠标移动
// 鼠标抬起
}
监听鼠标按下
当拖拽容器的边框按下时,就会触发鼠标按下的回调事件:
js
line.addEventListener('mousedown', (event) => {
if (!this.basicConfig[lineKey]) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
isResizing = true
width = dragBox.getBoundingClientRect().width
height = dragBox.getBoundingClientRect().height
if (lineKey === DARG_LINE.T_LINE) startCoord = event.clientY + height
if (lineKey === DARG_LINE.B_LINE) startCoord = event.clientY - height
if (lineKey === DARG_LINE.L_LINE) startCoord = event.clientX + width
if (lineKey === DARG_LINE.R_LINE) startCoord = event.clientX - width
left = width + dragBox.getBoundingClientRect().left - parentDom.getBoundingClientRect().left
top = height + dragBox.getBoundingClientRect().top - parentDom.getBoundingClientRect().top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
代码解释
-
if (!this.basicConfig[lineKey]) return
:检查this.basicConfig
对象中是否存在对应边框的的配置项,如果不存在则直接返回,不执行后续代码。 -
if (this.basicConfig.disableUserSelect) { ... }
:检查配置中的disableUserSelect
属性是否为真。如果是真,则执行以下代码块:this.disableUserSelect = document.body.style.userSelect
:保存当前文档的body
元素的userSelect
样式属性值到disableUserSelect
变量中。document.body.style.userSelect = 'none'
:将文档的body
元素的userSelect
样式属性设置为'none'
,禁止用户选择文本。
-
isResizing = true
:将isResizing
变量设置为true
,表示正在进行调整大小的操作。 -
width = dragBox.getBoundingClientRect().width
:获取dragBox
元素的宽度,并将其保存到width
变量中。 -
height = dragBox.getBoundingClientRect().height
:获取dragBox
元素的高度,并将其保存到height
变量中。 -
根据
lineKey
的值,计算出startCoord
的初始值。根据不同的拖动方向,startCoord
的值将不同:(startCoord
表示计算宽度时的真实位置,举个例子我拖动右侧拖动线,是可以基于当前线左右拖动的那么右侧拖动线的计算时相对的就应该是左侧的拖动线位置)- 如果
lineKey
为DARG_LINE.T_LINE
,则将startCoord
设置为event.clientY + height
,表示拖动顶部线的计算宽度时的真实位置。 - 如果
lineKey
为DARG_LINE.B_LINE
,则将startCoord
设置为event.clientY - height
,表示拖动底部线的计算宽度时的真实位置。 - 如果
lineKey
为DARG_LINE.L_LINE
,则将startCoord
设置为event.clientX + width
,表示左侧拖动线的计算宽度时的真实位置。 - 如果
lineKey
为DARG_LINE.R_LINE
,则将startCoord
设置为event.clientX - width
,表示右侧拖动线的计算宽度时的真实位置。
- 如果
-
根据元素的宽度、高度和位置,计算出
left
和top
的值,用于确定调整大小的元素的位置:left = width + dragBox.getBoundingClientRect().left - parentDom.getBoundingClientRect().left
:计算调整大小的元素的左偏移量。top = height + dragBox.getBoundingClientRect().top - parentDom.getBoundingClientRect().top
:计算调整大小的元素的上偏移量。
-
document.addEventListener('mousemove', onMouseMove)
:给整个文档添加鼠标移动事件的监听器,并将onMouseMove
函数作为回调函数。 -
document.addEventListener('mouseup', onMouseUp, { once: true })
:给整个文档添加鼠标释放事件的监听器,并将onMouseUp
函数作为回调函数,并设置{ once: true }
选项,表示该事件只触发一次。
监听鼠标移动
鼠标按下后会为节点绑定鼠标移动事件,边框的尺寸更新功能就是在鼠标移动的时候实时更新的。
鼠标移动函数:
js
const onMouseMove = (event) => {
if (isResizing) {
let curSize
const sizeLimit = isVertical ? [this.basicConfig.minHeight, this.basicConfig.maxHeight] : [this.basicConfig.minWidth, this.basicConfig.maxWidth]
if (lineKey === DARG_LINE.T_LINE) curSize = startCoord - event.clientY
if (lineKey === DARG_LINE.B_LINE) curSize = event.clientY - startCoord
if (lineKey === DARG_LINE.L_LINE) curSize = startCoord - event.clientX
if (lineKey === DARG_LINE.R_LINE) curSize = event.clientX - startCoord
if (this.lineEdgeCheck({ curSize, sizeLimit, lineKey, parentDom, clientY: event.clientY, clientX: event.clientX })) {
requestAnimationFrame(() => {
if (lineKey === DARG_LINE.R_LINE) this.$emit('update:width', (curSize - 10))
if (lineKey === DARG_LINE.B_LINE) this.$emit('update:height', (curSize - 10))
if (lineKey === DARG_LINE.T_LINE) {
dragBox.style.top = (top - curSize) + 'px' // 更新坐标
this.$emit('update:height', (curSize - 10))
}
if (lineKey === DARG_LINE.L_LINE) {
dragBox.style.left = (left - curSize) + 'px' // 更新坐标
this.$emit('update:width', (curSize - 10))
}
})
}
}
}
边界判断函数:
js
lineEdgeCheck (obj) {
// 是否超过边界
let isEdge = false
const parentDomPosition = obj.parentDom.getBoundingClientRect()
if (obj.lineKey === DARG_LINE.T_LINE) isEdge = obj.clientY >= (parentDomPosition.top + (this.basicConfig.safeDistance.top || 0))
if (obj.lineKey === DARG_LINE.B_LINE) isEdge = obj.clientY <= (parentDomPosition.bottom - (this.basicConfig.safeDistance.bottom || 0))
if (obj.lineKey === DARG_LINE.L_LINE) isEdge = obj.clientX >= (parentDomPosition.left + (this.basicConfig.safeDistance.left || 0))
if (obj.lineKey === DARG_LINE.R_LINE) isEdge = obj.clientX <= (parentDomPosition.right - (this.basicConfig.safeDistance.right || 0))
// 是否在区间中
const isInRange = (obj.sizeLimit[0] ? obj.curSize > obj.sizeLimit[0] : true) && (obj.sizeLimit[1] ? obj.curSize < obj.sizeLimit[1] : true)
return isInRange && isEdge
},
代码解释:
-
检查是否正在进行调整大小的操作:
- 通过检查变量
isResizing
的值,判断是否正在进行调整大小的操作。 - 只有当
isResizing
为true
时,才会进行后续的操作。
- 通过检查变量
-
确定尺寸限制的最小值和最大值:
- 根据拖动的方向(垂直或水平),确定尺寸限制的最小值和最大值。
- 如果是垂直方向的拖动,则使用
this.basicConfig.minHeight
和this.basicConfig.maxHeight
作为尺寸限制。 - 如果是水平方向的拖动,则使用
this.basicConfig.minWidth
和this.basicConfig.maxWidth
作为尺寸限制。
-
计算当前大小变化的值
curSize
:-
根据拖动线的类型,计算当前大小变化的值
curSize
。 -
通过对比起始坐标
startCoord
和鼠标当前的坐标event.clientY
或event.clientX
,计算出当前大小相对于初始大小的变化值curSize
。 -
具体的计算方式根据拖动线的类型有所不同(因为计算变更长度时是根据对边计算的):
- 如果是顶部拖动线(
DARG_LINE.T_LINE
),计算curSize = startCoord - event.clientY
。 - 如果是底部拖动线(
DARG_LINE.B_LINE
),计算curSize = event.clientY - startCoord
。 - 如果是左侧拖动线(
DARG_LINE.L_LINE
),计算curSize = startCoord - event.clientX
。 - 如果是右侧拖动线(
DARG_LINE.R_LINE
),计算curSize = event.clientX - startCoord
。
- 如果是顶部拖动线(
-
-
调用
lineEdgeCheck
方法进行边界和尺寸限制的检查:- 调用名为
lineEdgeCheck
的方法,传入一个包含相关参数的对象obj
作为参数。 - 这个方法的作用是判断当前大小变化是否超过了边界,并且是否在最小和最大尺寸限制范围内。
- 方法会根据拖动线的类型和边界位置,判断当前的鼠标位置是否超过了边界。
- 方法还会根据最小和最大尺寸限制,判断当前大小变化值
curSize
是否在有效范围内。 - 如果同时满足边界和尺寸限制条件,
lineEdgeCheck
方法返回true
,表示可以进行调整大小的操作。
- 调用名为
-
执行大小变化的操作:
-
如果
lineEdgeCheck
返回true
,表示当前大小变化在有效范围内,可以进行调整大小的操作。 -
使用
requestAnimationFrame
函数在下一帧进行更新操作,以提高性能。 -
根据拖动线的类型,执行相应的操作来更新元素的大小或位置:
- 如果是右侧拖动线(
DARG_LINE.R_LINE
),通过触发'update:width'
事件并传入(curSize - 10)
作为新的宽度值来更新宽度,因为坐标没变所以直接更新即可。 - 如果是底部拖动线(
DARG_LINE.B_LINE
),通过触发'update:height'
事件并传入(curSize - 10)
作为新的高度值来更新高度,因为坐标没变所以直接更新即可。 - 如果是顶部拖动线(
DARG_LINE.T_LINE
),通过更新元素的top
样式属性,使元素的位置上移,并通过触发'update:height'
事件并传入(curSize - 10)
作为新的高度值来更新高度,因为坐标变了,所以要先更新坐标再改高度。 - 如果是左侧拖动线(
DARG_LINE.L_LINE
),通过更新元素的left
样式属性,使元素的位置左移,并通过触发'update:width'
事件并传入(curSize - 10)
作为新的宽度值来更新宽度,因为坐标变了,所以要先更新坐标再改宽度。
- 如果是右侧拖动线(
-
-
lineEdgeCheck
方法的功能:lineEdgeCheck
是一个独立的方法,用于判断当前大小变化是否超过了边界,并且是否在最小和最大尺寸限制范围内。- 该方法接收一个包含相关参数的对象
obj
作为参数,包括当前大小变化值curSize
、尺寸限制范围、拖动线类型、父元素以及鼠标的垂直坐标和水平坐标。 - 方法根据拖动线的类型和边界位置,判断当前的鼠标位置是否超过了边界。
- 方法还根据最小和最大尺寸限制,判断当前大小变化值
curSize
是否在有效范围内。 - 如果同时满足边界和尺寸限制条件,
lineEdgeCheck
方法返回true
,表示可以进行调整大小的操作。 - 否则,返回
false
,表示不应进行调整大小的操作。
监听鼠标抬起
这一步比较简单,只要把监听事件移除就行,,同上,注意的是在上面我们加了useSelect,所以要回写回去。
js
const onMouseUp = () => {
isResizing = false
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
完整拖拽边框函数
js
// 遍历初始化
resizeLineInit () {
const dragBox = (this.$refs.dragBox as HTMLElement)
this.RESIZE_LINE_ARR.forEach(item => {
this.lineInit(this.$refs[item] as HTMLElement, dragBox, item)
})
}
// 初始化
lineInit (line: HTMLElement, dragBox: HTMLElement, lineKey:DARG_LINE) {
let width, height, left, top
// 起始位置
let startCoord = 0
let isResizing = false
// 父节点
const parentDom = this.basicConfig.parentDom
// 是否操作垂直方向
const isVertical = [DARG_LINE.T_LINE, DARG_LINE.B_LINE].includes(lineKey)
line.addEventListener('mousedown', (event) => {
if (!this.basicConfig[lineKey]) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
isResizing = true
width = dragBox.getBoundingClientRect().width
height = dragBox.getBoundingClientRect().height
if (lineKey === DARG_LINE.T_LINE) startCoord = event.clientY + height
if (lineKey === DARG_LINE.B_LINE) startCoord = event.clientY - height
if (lineKey === DARG_LINE.L_LINE) startCoord = event.clientX + width
if (lineKey === DARG_LINE.R_LINE) startCoord = event.clientX - width
left = width + dragBox.getBoundingClientRect().left - parentDom.getBoundingClientRect().left
top = height + dragBox.getBoundingClientRect().top - parentDom.getBoundingClientRect().top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
const onMouseMove = (event) => {
if (isResizing) {
let curSize
// 获取垂直方向或水平方向,最大最小值
const sizeLimit = isVertical ? [this.basicConfig.minHeight, this.basicConfig.maxHeight] : [this.basicConfig.minWidth, this.basicConfig.maxWidth]
if (lineKey === DARG_LINE.T_LINE) curSize = startCoord - event.clientY
if (lineKey === DARG_LINE.B_LINE) curSize = event.clientY - startCoord
if (lineKey === DARG_LINE.L_LINE) curSize = startCoord - event.clientX
if (lineKey === DARG_LINE.R_LINE) curSize = event.clientX - startCoord
if (this.lineEdgeCheck({ curSize, sizeLimit, lineKey, parentDom, clientY: event.clientY, clientX: event.clientX })) {
requestAnimationFrame(() => {
if (lineKey === DARG_LINE.R_LINE) this.$emit('update:width', (curSize - 10))
if (lineKey === DARG_LINE.B_LINE) this.$emit('update:height', (curSize - 10))
if (lineKey === DARG_LINE.T_LINE) {
dragBox.style.top = (top - curSize) + 'px' // 更新坐标
this.$emit('update:height', (curSize - 10))
}
if (lineKey === DARG_LINE.L_LINE) {
dragBox.style.left = (left - curSize) + 'px' // 更新坐标
this.$emit('update:width', (curSize - 10))
}
})
}
}
}
const onMouseUp = () => {
isResizing = false
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
},
// 边界校验
lineEdgeCheck (obj) {
// 判断是否超过边界
let isEdge = false
const parentDomPosition = obj.parentDom.getBoundingClientRect()
if (obj.lineKey === DARG_LINE.T_LINE) isEdge = obj.clientY >= (parentDomPosition.top + (this.basicConfig.safeDistance.top || 0))
if (obj.lineKey === DARG_LINE.B_LINE) isEdge = obj.clientY <= (parentDomPosition.bottom - (this.basicConfig.safeDistance.bottom || 0))
if (obj.lineKey === DARG_LINE.L_LINE) isEdge = obj.clientX >= (parentDomPosition.left + (this.basicConfig.safeDistance.left || 0))
if (obj.lineKey === DARG_LINE.R_LINE) isEdge = obj.clientX <= (parentDomPosition.right - (this.basicConfig.safeDistance.right || 0))
// 判断是否在最大最小区间中
const isInRange = (obj.sizeLimit[0] ? obj.curSize > obj.sizeLimit[0] : true) && (obj.sizeLimit[1] ? obj.curSize < obj.sizeLimit[1] : true)
return isInRange && isEdge
},
完整组件代码
html
<template>
<div v-if="visible" ref="dragBox" class="main" :style="{ zIndex: zIndex, ...basicConfig.position }">
<div :ref="DARG_LINE.T_LINE" class="resize-y border-t"></div>
<div class="g-flex">
<div :ref="DARG_LINE.L_LINE" class="resize-x border-l"></div>
<div ref="dragBoxContent" :style="{ width: width + 'px', height: height + 'px' }">
<slot name="content"></slot>
</div>
<div :ref="DARG_LINE.R_LINE" class="resize-x border-r"></div>
</div>
<div :ref="DARG_LINE.B_LINE" class="resize-y border-b"></div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export enum DARG_LINE {
T_LINE = 'topLine',
B_LINE = 'bottomLine',
R_LINE = 'rightLine',
L_LINE = 'leftLine'
}
// 容器开始位置
export type POSITION = {
top?: string | number
left?: string | number
bottom?: string | number
right?: string | number
}
// 安全距离
export type SAFE_DISTANCE = {
top?: number
left?: number
bottom?: number
right?: number
}
// 容器类型
export type DragBoxType = {
parentDom: HTMLElement
minHeight: string | number
maxHeight: string | number
minWidth: string | number
maxWidth: string | number
canDrag?: boolean
position?: POSITION
safeDistance?: SAFE_DISTANCE
horizontalMove?: boolean
verticalMove?: boolean
disableUserSelect?: boolean
[DARG_LINE.B_LINE]?: boolean
[DARG_LINE.L_LINE]?: boolean
[DARG_LINE.T_LINE]?: boolean
[DARG_LINE.R_LINE]?: boolean
}
export default defineComponent({
props: {
visible: {
type: Boolean,
default: false
},
config: {
type: Object as PropType<DragBoxType>,
default: () => {
return {} as DragBoxType
}
},
zIndex: {
type: String,
default: '100'
},
height: {
type: [String, Number],
default: '0'
},
width: {
type: [String, Number],
default: '0'
}
},
data () {
return {
DARG_LINE,
startResize: false,
RESIZE_LINE_ARR: [DARG_LINE.B_LINE, DARG_LINE.L_LINE, DARG_LINE.R_LINE, DARG_LINE.T_LINE],
disableUserSelect: ''
}
},
computed: {
/**
* @description: 初始化参数
* @return {*}
*/
basicConfig () {
return {
position: {
top: '0',
left: '0'
},
safeDistance: {},
width: 0,
height: 0,
horizontalMove: true,
verticalMove: true,
canDrag: true,
...this.config
}
}
},
mounted () {
this.$nextTick(() => {
this.resizeLineInit()
this.dragInit()
})
},
methods: {
dragInit () {
const parentDom = this.basicConfig.parentDom
const dragBoxContent = (this.$refs.dragBoxContent as HTMLElement)
const dragBox = (this.$refs.dragBox as HTMLElement)
let startX, startY, initialLeft, initialTop
dragBoxContent.addEventListener('mousedown', (event) => {
if (!this.basicConfig.canDrag) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
startX = event.clientX
startY = event.clientY
const { left, top } = dragBoxContent.getBoundingClientRect()
initialLeft = left
initialTop = top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
const onMouseMove = (event) => {
const offsetX = event.clientX - startX
const offsetY = event.clientY - startY
const newLeft = initialLeft + offsetX
const newTop = initialTop + offsetY
const newRight = newLeft + dragBox.getBoundingClientRect().width
const newBottom = newTop + dragBox.getBoundingClientRect().height
if (newLeft >= (parentDom.getBoundingClientRect().left + (this.basicConfig.safeDistance.left || 0)) && newRight <= (parentDom.getBoundingClientRect().right - (this.basicConfig.safeDistance.right || 0)) && this.basicConfig.horizontalMove) {
dragBox.style.left = `${newLeft - parentDom.getBoundingClientRect().left}px`
}
if (newTop >= (parentDom.getBoundingClientRect().top + (this.basicConfig.safeDistance.top || 0)) && newBottom <= (parentDom.getBoundingClientRect().bottom - (this.basicConfig.safeDistance.bottom || 0)) && this.basicConfig.verticalMove) {
dragBox.style.top = `${newTop - parentDom.getBoundingClientRect().top}px`
}
}
const onMouseUp = (_event) => {
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
},
resizeLineInit () {
const dragBox = (this.$refs.dragBox as HTMLElement)
this.RESIZE_LINE_ARR.forEach(item => {
this.lineInit(this.$refs[item] as HTMLElement, dragBox, item)
})
}
lineInit (line: HTMLElement, dragBox: HTMLElement, lineKey:DARG_LINE) {
let width, height, left, top
// 起始位置
let startCoord = 0
let isResizing = false
// 父节点
const parentDom = this.basicConfig.parentDom
// 是否操作垂直方向
const isVertical = [DARG_LINE.T_LINE, DARG_LINE.B_LINE].includes(lineKey)
line.addEventListener('mousedown', (event) => {
if (!this.basicConfig[lineKey]) return
if (this.basicConfig.disableUserSelect) {
this.disableUserSelect = document.body.style.userSelect
document.body.style.userSelect = 'none'
}
isResizing = true
width = dragBox.getBoundingClientRect().width
height = dragBox.getBoundingClientRect().height
if (lineKey === DARG_LINE.T_LINE) startCoord = event.clientY + height
if (lineKey === DARG_LINE.B_LINE) startCoord = event.clientY - height
if (lineKey === DARG_LINE.L_LINE) startCoord = event.clientX + width
if (lineKey === DARG_LINE.R_LINE) startCoord = event.clientX - width
left = width + dragBox.getBoundingClientRect().left - parentDom.getBoundingClientRect().left
top = height + dragBox.getBoundingClientRect().top - parentDom.getBoundingClientRect().top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp, { once: true })
})
const onMouseMove = (event) => {
if (isResizing) {
let curSize
// 获取垂直方向或水平方向,最大最小值
const sizeLimit = isVertical ? [this.basicConfig.minHeight, this.basicConfig.maxHeight] : [this.basicConfig.minWidth, this.basicConfig.maxWidth]
if (lineKey === DARG_LINE.T_LINE) curSize = startCoord - event.clientY
if (lineKey === DARG_LINE.B_LINE) curSize = event.clientY - startCoord
if (lineKey === DARG_LINE.L_LINE) curSize = startCoord - event.clientX
if (lineKey === DARG_LINE.R_LINE) curSize = event.clientX - startCoord
if (this.lineEdgeCheck({ curSize, sizeLimit, lineKey, parentDom, clientY: event.clientY, clientX: event.clientX })) {
requestAnimationFrame(() => {
if (lineKey === DARG_LINE.R_LINE) this.$emit('update:width', (curSize - 10))
if (lineKey === DARG_LINE.B_LINE) this.$emit('update:height', (curSize - 10))
if (lineKey === DARG_LINE.T_LINE) {
dragBox.style.top = (top - curSize) + 'px' // 更新坐标
this.$emit('update:height', (curSize - 10))
}
if (lineKey === DARG_LINE.L_LINE) {
dragBox.style.left = (left - curSize) + 'px' // 更新坐标
this.$emit('update:width', (curSize - 10))
}
})
}
}
}
const onMouseUp = () => {
isResizing = false
if (this.basicConfig.disableUserSelect) document.body.style.userSelect = this.disableUserSelect
document.removeEventListener('mousemove', onMouseMove)
}
},
lineEdgeCheck (obj) {
// 判断是否超过边界
let isEdge = false
const parentDomPosition = obj.parentDom.getBoundingClientRect()
if (obj.lineKey === DARG_LINE.T_LINE) isEdge = obj.clientY >= (parentDomPosition.top + (this.basicConfig.safeDistance.top || 0))
if (obj.lineKey === DARG_LINE.B_LINE) isEdge = obj.clientY <= (parentDomPosition.bottom - (this.basicConfig.safeDistance.bottom || 0))
if (obj.lineKey === DARG_LINE.L_LINE) isEdge = obj.clientX >= (parentDomPosition.left + (this.basicConfig.safeDistance.left || 0))
if (obj.lineKey === DARG_LINE.R_LINE) isEdge = obj.clientX <= (parentDomPosition.right - (this.basicConfig.safeDistance.right || 0))
// 判断是否在最大最小区间中
const isInRange = (obj.sizeLimit[0] ? obj.curSize > obj.sizeLimit[0] : true) && (obj.sizeLimit[1] ? obj.curSize < obj.sizeLimit[1] : true)
return isInRange && isEdge
},
}
})
</script>
<style lang="scss" scoped>
$hoverColor: #1d7dfa;
@mixin hoverBorder($property, $direction) {
&.border#{$direction} {
&:hover {
border-#{$property}: 1px dashed $hoverColor;
}
}
}
.main {
position: absolute;
width: fit-content;
height: fit-content;
}
.border-t {
@include hoverBorder(top, -t);
}
.border-b {
@include hoverBorder(bottom, -b);
}
.border-l {
@include hoverBorder(left, -l);
}
.border-r {
@include hoverBorder(right, -r);
}
.resize-x {
width: 5px;
cursor: col-resize;
}
.resize-y {
height: 5px;
cursor: row-resize;
}
</style>