当前位置: 首页 > news >正文

【vue】聊一聊拖拽改变DOM大小的实现

背景

大家或多或少应该有遇到过通过拖拽改变DOM大小的需求:比如说页面的侧边栏支持拖拽调整大小,使内容可视区变大。之所以讲这个需求,是因为在我个人开发的开源项目中有做到这个需求,所以在这里和大家聊一聊我的实现。以下内容均以向右拖拽为例。

思路

既然要实现拖拽,那就需要在DOM内有一个支持拖拽的节点,然后通过addEventListener来监听鼠标点击鼠标点击后移动事件鼠标松开事件。在鼠标点击后监听移动事件,在移动事件中计算鼠标点击位置与移动后的位置间的距离:这个距离就是DOM需要增加或者减少的宽度。

简单实现

思路清楚了,剩下的就是实现了,最主要的就是计算的部分了。贴上实现代码:

<template>
  <div class="container">
    <div>
      <div ref="box" class="box">
        <div ref="drag" class="drag"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"

const box = ref()
const drag = ref()

const width = ref()
const downPageX = ref()

const moveHandle = (event) => {
  document.documentElement.style.cursor = 'col-resize'
  const value = event.pageX - downPageX.value
  const size = width.value + value
  box.value.style.width = size + 'px'
}

const upHandle = () => {
  // 移除监听
  document.removeEventListener('mousemove', moveHandle)
  document.removeEventListener('mouseup', upHandle)
  // 移除鼠标样式
  document.documentElement.style.cursor = ''
}

const downListener = () => {
  drag.value.addEventListener('mousedown', (event) => {
    // 获取水平参数
    width.value = box.value.offsetWidth
    downPageX.value = event.pageX
    // 添加鼠标移动事件
    document.addEventListener('mousemove', moveHandle)
    // 添加鼠标松开事件
    document.addEventListener('mouseup', upHandle)
  })
}

onMounted(() => {
  downListener()
})

onBeforeUnmount(() => {
  // 移除监听
  document.removeEventListener('mousemove', moveHandle)
  document.removeEventListener('mouseup', upHandle)
  // 移除鼠标样式
  document.documentElement.style.cursor = ''
})

</script>

<style scoped>
// 省略...
</style>

拖拽指令

上面已经完成了简单的实现,但是在我们实际开发过程中不会就某一个地方需要用到这个实现。虽然可以CV,但这绝对不是最优解,在这基础之上进一步优化。

我们可以将这个功能封装成自定义指令,思路与上文的思路相差无几。在所有节点都挂载完成之后,在使用了指令的节点里面插入一个可拖拽的节点,然后通过addEventListener来监听各个鼠标事件,再进行一些计算,最后在节点卸载后移除相关监听。

本着精益求精的态度,必然不能只做到上面提到的那些。在实际开发中或许会遇到一些其它情况:比如我们拖拽的方向、可拖拽的最大最小距离、具体事件的回调等等问题都需要考虑在内。就有了以下完整的实现:

/**
 * @description: 拖拽改变大小指令
 * @param {*}
 * @return {*}
 * @author: gumingchen
 */
const horizontal = ['left', 'right']
const vertical = ['top', 'bottom']
const positions = [...horizontal, ...vertical]

// 默认参数
const defaultOptions = {
  // 拖拽位置
  position: positions[1],
  // 拖拽区域大小
  areaSize: '3px',
  // 拖拽区域背景色
  areaBackground: 'transparent',
  // Element的最小宽度/高度
  min: 200,
  // Element的最大宽度/高度
  max: 460,
  // 处理器
  downHandler: null,
  moveHandler: null,
  upHandler: null
}

// 初始化数据
let data = {
  el: null,
  dom: null,
  options: null,

  width: null,
  downPageX: null,
  height: null,
  downPageY: null
}

/**
 * 拖拽区域位置处理
 * @param {*} div
 * @param {*} position
 */
const positionHandle = (div, position, size) => {
  // 水平方向
  if (horizontal.includes(position)) {
    div.style.top = '0px'
    div.style.bottom = '0px'
    div.style[position] = '0px'
    div.style.cursor = 'col-resize'
    div.style.width = size
  }
  // 垂直方向
  if (vertical.includes(position)) {
    div.style.right = '0px'
    div.style.left = '0px'
    div.style[position] = '0px'
    div.style.cursor = 'row-resize'
    div.style.height = size
  }
}

/**
 * 创建节点
 * @returns Element
 */
const createElement = (options) => {
  const div = document.createElement('div')
  div.style.position = 'absolute'
  div.style.userSelect = 'none'

  const { position, areaSize, areaBackground } = options

  positionHandle(div, position, areaSize)

  div.style.background = areaBackground

  return div
}

/**
 * 鼠标移动监听事件
 * @param {*} event
 * @returns
 */
const moveHandle = (event) => {
  const { el, dom, options, width, downPageX, height, downPageY } = data
  const { moveHandler, position, max, min } = options
  // 事件回调
  if (typeof moveHandler === 'function') {
    moveHandler(event)
  }
  // 设置鼠标样式
  document.documentElement.style.cursor = dom.style.cursor
  // 水平方向处理
  if (horizontal.includes(position)) {
    const value = position === horizontal[0] ? downPageX - event.pageX : event.pageX - downPageX
    const size = width + value
    if (size >= max) {
      return
    }
    if (size <= min) {
      return
    }
    el.style.width = size + 'px'
  }
  // 垂直方向处理
  if (vertical.includes(position)) {
    const value = position === vertical[0] ? downPageY - event.pageY : event.pageY - downPageY
    const size = height + value
    if (size >= max) {
      return
    }
    if (size <= min) {
      return
    }
    el.style.height = size + 'px'
  }
}
/**
 * 鼠标松开监听事件
 * @param {*} event
 */
const upHandle = (event) => {
  // 事件回调
  const { upHandler } = data.options
  if (typeof upHandler === 'function') {
    upHandler(event)
  }
  // 移除监听
  document.removeEventListener('mousemove', moveHandle)
  document.removeEventListener('mouseup', upHandle)
  // 移除鼠标样式
  document.documentElement.style.cursor = ''
}

/**
 * 鼠标按下事件
 * @param {*} el 当前节点
 * @param {*} dom 可拖拽节点
 * @param {*} options 参数
 */
const downListener = (el, dom, options) => {
  dom.addEventListener('mousedown', (event) => {
    // 事件回调
    const { downHandler } = options
    if (typeof downHandler === 'function') {
      downHandler(event)
    }
    // 获取水平参数
    const width = el.offsetWidth
    const downPageX = event.pageX
    // 获取垂直参数
    const height = el.offsetHeight
    const downPageY = event.pageY
    // 设置数据
    data = { el, dom, options, width, downPageX, height, downPageY }
    // 添加鼠标移动事件
    document.addEventListener('mousemove', moveHandle)
    // 添加鼠标松开事件
    document.addEventListener('mouseup', upHandle)
  })
}

export default {
  mounted(el, binding) {
    el.style.position = 'relative'
    const { arg, value } = binding

    const options = value ? { ...defaultOptions, ...value } : { ...defaultOptions }
    if (arg) {
      options.position = arg
    }

    if (!positions.includes(options.position)) {
      console.warn(`[Directive warn]: Invalid arg: validation failed for arg. Expected one of ${ JSON.stringify(positions) }, got value "${ options.position }".`)
      return
    }

    const dom = createElement(options)

    downListener(el, dom, options)

    el.appendChild(dom)
  },
  unmounted() {
    // 移除监听
    document.removeEventListener('mousemove', moveHandle)
    document.removeEventListener('mouseup', upHandle)
    // 移除鼠标样式
    document.documentElement.style.cursor = ''
  }
}

以上就是本次分享的内容,附上源码。

感谢看官看到这里,如果觉得文章不错的话,可以给小生的几个开源项目点个Star⭐!

  • 基于 Vue3 + Element-plus 管理后台基础功能框架
    • 预览:http://admin.gumingchen.icu
    • Github:https://github.com/gmingchen/agile-admin
    • Gitee:https://gitee.com/shychen/agile-admin
    • 基础版后端:https://github.com/gmingchen/java-spring-boot-admin
    • 文档:http://admin.gumingchen.icu/doc/
  • 基于 Vue3 + Element-plus + websocket 即时聊天系统
    • 预览:https://chatterbox.gumingchen.icu/
    • 代码:https://github.com/gmingchen/chatterbox
  • 基于 node 开发的后端服务:https://github.com/gmingchen/node-server
http://www.dtcms.com/a/99730.html

相关文章:

  • Redis场景问题2:缓存击穿
  • VMware笔记之windows的vmware17pro中的ubuntu22.04调整系统空间大小
  • C#里实现C#脚本单步执行的信息提示
  • 算法 之 求解有向图和无向图的环的长度
  • CSS学习笔记4——盒子模型
  • IO模型之于并发编程模型、并发模型之于架构模式
  • 破界·共生:生成式人工智能(GAI)认证重构普通人的AI进化图谱
  • SpringCould微服务架构之Docker(6)
  • 【C#】C#字符串拼接的6种方式及其性能分析对比
  • Axure项目实战:智慧运输平台后台管理端-货主管理(中继器)
  • 21 python __name__ 与 __main__
  • Java中的String类
  • 智能巡检机器人:2025年企业安全运维的“数字哨兵“
  • Vue 3 中 slot插槽的使用方法
  • 最大子序和 买股票的最佳时机|| 跳跃游戏
  • 【计算机网络】深入解析TCP/IP参考模型:从四层架构到数据封装,全面对比OSI
  • 面经-项目
  • 革新测试管理 2.0丨Storm UTP统一测试管理平台智能化升级与全流程优化
  • HCIP之VRRP
  • 晶晨S905L3A(B)-安卓9.0-开启ADB和ROOT-支持IPTV6-支持外置游戏系统-支持多种无线芯片-支持救砖-完美通刷线刷固件包
  • memtest86检测内存
  • Anaconda Jupyter 默认启动位置修改
  • 矩阵中对角线的遍历问题【C++】
  • JavaScript运算符与逻辑中断
  • 从零到前沿:2025年人工智能系统性学习路径与最新技术融合指南
  • LangChain 基础系列之文档加载与分割详解:从非结构化数据到知识图谱的关键一步
  • ubuntu24 部署vnc server 使用VNC Viewer连接
  • vLLM 实现加速的原理及举例; vLLM 与 TensorRT 的区别
  • C#里使用C#语言作为脚本运行的方法
  • HarmonyOS NEXT——鸿蒙神策埋点(二)