封装一个拖拽容器组件,支持拖拽移动,可以通过拖拽边框更新组件大小

前言

最近在接到一个需求,需要在一个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 })
})
  1. 首先检查是否允许拖拽操作。

  2. 接下来,检查是否禁用了用户选择(disableUserSelect)。如果禁用了,先保存当前的用户选择样式(document.body.style.userSelect)到变量this.disableUserSelect中,并将文档的用户选择样式设置为'none',保证拖拽过程中用户无法选择文本。

  3. 然后,记录鼠标按下时的坐标(event.clientX和event.clientY),作为拖拽操作的起始点。

  4. 接下来,获取拖拽框的当前位置(left和top)使用getBoundingClientRect()方法,并将其记录为初始位置(initialLeft和initialTop)。用于后续计算新位置。

  5. 最后,它向文档添加了两个事件监听器: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`
  }
}
  1. 首先,用当前鼠标位置(event.clientX和event.clientY)减去拖拽操作开始时的位置(startX和startY),得到鼠标在X轴和Y轴上的偏移量(offsetX和offsetY)。

  2. 接下来,根据初始位置(initialLeft和initialTop)和偏移量(offsetX和offsetY)计算出拖拽框的新位置。

    新的左边界(newLeft)等于初始左边界加上X轴偏移量,新的顶部边界(newTop)等于初始顶部边界加上Y轴偏移量。

    根据新位置计算出容器的新右边界(newRight)和新底部边界(newBottom),再使用getBoundingClientRect()方法获取dragBox的宽度和高度,并将其加到新的左边界和顶部边界上,得到新的右边界和底部边界。

    这样就可以获得四边坐标(更新坐标只需要newTop和newLeft就行,拿四边坐标主要是为了做边界判断)。

  3. 进行边界判断。

    (1)新的左边界是否大于等于父元素(parentDom)的左边界加上安全距离(如果没有指定安全距离,则默认为0)。

    (2)新的右边界是否小于等于父元素的右边界减去安全距离(如果没有指定安全距离,则默认为0)

    (3)this.basicConfig.horizontalMove是否为真。

    如果满足这些条件,表示拖拽框可以在水平方向上移动,那么函数会根据新的左边界和父元素的左边界之差,将拖拽框的左边距样式(dragBox.style.left)设置为相应的值,下同

  4. 类似地,函数还检查新的顶部边界是否大于等于父元素的顶部边界加上安全距离,新的底部边界是否小于等于父元素的底部边界减去安全距离,以及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
},

代码解释:

  1. 检查是否正在进行调整大小的操作:

    • 通过检查变量isResizing的值,判断是否正在进行调整大小的操作。
    • 只有当isResizing为true时,才会进行后续的操作。
  2. 确定尺寸限制的最小值和最大值:

    • 根据拖动的方向(垂直或水平),确定尺寸限制的最小值和最大值。
    • 如果是垂直方向的拖动,则使用this.basicConfig.minHeight和this.basicConfig.maxHeight作为尺寸限制。
    • 如果是水平方向的拖动,则使用this.basicConfig.minWidth和this.basicConfig.maxWidth作为尺寸限制。
  3. 计算当前大小变化的值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。
  4. 调用lineEdgeCheck方法进行边界和尺寸限制的检查:

    • 调用名为lineEdgeCheck的方法,传入一个包含相关参数的对象obj作为参数。
    • 这个方法的作用是判断当前大小变化是否超过了边界,并且是否在最小和最大尺寸限制范围内。
    • 方法会根据拖动线的类型和边界位置,判断当前的鼠标位置是否超过了边界。
    • 方法还会根据最小和最大尺寸限制,判断当前大小变化值curSize是否在有效范围内。
    • 如果同时满足边界和尺寸限制条件,lineEdgeCheck方法返回true,表示可以进行调整大小的操作。
  5. 执行大小变化的操作:

    • 如果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)作为新的宽度值来更新宽度,因为坐标变了,所以要先更新坐标再改宽度。
  6. 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>
相关推荐
卸任18 小时前
Vibe coding工具使用心得
前端·ai编程
brien919 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance19 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M19 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写19 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李20 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易20 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马20 小时前
前沿视界
前端
kyriewen20 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易20 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员