手写VueUse的useDraggable函数实现

以下是手写的 useDraggable 函数实现,用于实现元素的拖拽功能:

typescript 复制代码
import {
  ref,
  onMounted,
  onUnmounted,
  unref,
  type Ref,
  type ComponentPublicInstance
} from 'vue'

interface UseDraggableOptions {
  initialValue?: { x: number; y: number }
  axis?: 'both' | 'x' | 'y'
  bounds?: {
    left?: number
    right?: number
    top?: number
    bottom?: number
  }
}

export function useDraggable(
  target: Ref<HTMLElement | ComponentPublicInstance | null>,
  options: UseDraggableOptions = {}
) {
  const { initialValue = { x: 0, y: 0 }, axis = 'both', bounds } = options
  const x = ref(initialValue.x)
  const y = ref(initialValue.y)
  const isDragging = ref(false)

  let element: HTMLElement | null = null
  let startX = 0
  let startY = 0
  let initialX = 0
  let initialY = 0

  const getElement = (): HTMLElement | null => {
    const el = unref(target)
    return (el as ComponentPublicInstance)?.$el ?? el
  }

  const applyTransform = () => {
    if (!element) return
    element.style.transform = `translate(${x.value}px, ${y.value}px)`
  }

  const clamp = (value: number, min?: number, max?: number) => {
    if (typeof min === 'number') value = Math.max(value, min)
    if (typeof max === 'number') value = Math.min(value, max)
    return value
  }

  const onStart = (clientX: number, clientY: number) => {
    isDragging.value = true
    startX = clientX
    startY = clientY
    initialX = x.value
    initialY = y.value
  }

  const onMove = (clientX: number, clientY: number) => {
    if (!isDragging.value) return

    let deltaX = clientX - startX
    let deltaY = clientY - startY

    if (axis === 'x') deltaY = 0
    if (axis === 'y') deltaX = 0

    let newX = initialX + deltaX
    let newY = initialY + deltaY

    // 边界限制
    if (bounds) {
      newX = clamp(newX, bounds.left, bounds.right)
      newY = clamp(newY, bounds.top, bounds.bottom)
    }

    x.value = newX
    y.value = newY
    applyTransform()
  }

  const onEnd = () => {
    isDragging.value = false
  }

  const handleMouseDown = (e: MouseEvent) => {
    e.preventDefault()
    onStart(e.clientX, e.clientY)
  }

  const handleTouchStart = (e: TouchEvent) => {
    const touch = e.touches[0]
    if (touch) onStart(touch.clientX, touch.clientY)
  }

  const addEventListeners = () => {
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
    document.addEventListener('touchmove', handleTouchMove)
    document.addEventListener('touchend', handleTouchEnd)
  }

  const removeEventListeners = () => {
    document.removeEventListener('mousemove', handleMouseMove)
    document.removeEventListener('mouseup', handleMouseUp)
    document.removeEventListener('touchmove', handleTouchMove)
    document.removeEventListener('touchend', handleTouchEnd)
  }

  const handleMouseMove = (e: MouseEvent) => onMove(e.clientX, e.clientY)
  const handleTouchMove = (e: TouchEvent) => {
    const touch = e.touches[0]
    if (touch) onMove(touch.clientX, touch.clientY)
  }

  const handleMouseUp = () => onEnd()
  const handleTouchEnd = () => onEnd()

  const init = () => {
    element = getElement()
    if (!element) return

    element.style.cursor = 'grab'
    element.style.userSelect = 'none'
    element.style.touchAction = 'none'

    element.addEventListener('mousedown', handleMouseDown)
    element.addEventListener('touchstart', handleTouchStart)
  }

  const cleanup = () => {
    if (!element) return

    element.removeEventListener('mousedown', handleMouseDown)
    element.removeEventListener('touchstart', handleTouchStart)
    element.style.cursor = ''
    element.style.userSelect = ''
    element.style.touchAction = ''
  }

  const stop = () => {
    cleanup()
    removeEventListeners()
  }

  onMounted(() => {
    init()
    applyTransform()
  })

  onUnmounted(() => {
    stop()
  })

  return {
    x,
    y,
    isDragging,
    stop,
    start: () => init(),
    applyTransform
  }
}

使用示例:

vue 复制代码
<template>
  <div ref="draggableEl" class="draggable-box">
    拖拽我
  </div>
  <div class="position-info">
    X: {{ x }}px, Y: {{ y }}px
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from './useDraggable'

const draggableEl = ref<HTMLElement | null>(null)

const { x, y, isDragging } = useDraggable(draggableEl, {
  initialValue: { x: 100, y: 100 },
  axis: 'both',
  bounds: {
    left: 0,
    top: 0,
    right: window.innerWidth - 200,
    bottom: window.innerHeight - 200
  }
})
</script>

<style>
.draggable-box {
  width: 200px;
  height: 200px;
  background-color: #42b883;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  position: fixed;
}

.position-info {
  position: fixed;
  bottom: 20px;
  left: 20px;
  padding: 10px;
  background: rgba(0,0,0,0.7);
  color: white;
  border-radius: 4px;
}
</style>

实现细节说明:

  1. 核心功能

    • 支持鼠标和触摸屏操作
    • 可限制拖拽方向(x/y轴或双向)
    • 支持设置拖拽边界
    • 自动处理CSS transform应用
  2. 响应式数据

    • x: 水平位移
    • y: 垂直位移
    • isDragging: 当前拖拽状态
  3. 实现亮点

    typescript 复制代码
    // 边界限制计算
    const clamp = (value: number, min?: number, max?: number) => {
      if (typeof min === 'number') value = Math.max(value, min)
      if (typeof max === 'number') value = Math.min(value, max)
      return value
    }

    确保元素始终保持在指定边界内

  4. 事件处理优化

    • 使用文档级事件监听器处理移动事件,避免快速拖拽时丢失事件
    • 同时支持鼠标和触摸事件
    typescript 复制代码
    // 统一事件处理
    const onStart = (clientX: number, clientY: number) => {
      // 记录初始位置
    }
    
    const onMove = (clientX: number, clientY: number) => {
      // 计算新位置
    }
  5. 样式处理

    typescript 复制代码
    // 自动应用transform
    const applyTransform = () => {
      if (!element) return
      element.style.transform = `translate(${x.value}px, ${y.value}px)`
    }

    使用transform实现高性能位移动画

  6. 生命周期管理

    typescript 复制代码
    onMounted(() => {
      init()
      applyTransform()
    })
    
    onUnmounted(() => {
      stop()
    })

    确保组件挂载时初始化,卸载时清理资源

  7. 类型安全

    • 完整的TypeScript类型定义
    • 支持Vue组件实例和原生DOM元素

配置选项说明:

参数 类型 默认值 说明
initialValue { x: number; y: number } { x:0, y:0 } 初始位置
axis 'both' | 'x' | 'y' 'both' 允许拖拽的方向
bounds { left?, right?, top?, bottom? } undefined 拖拽边界限制

返回API说明:

属性/方法 类型 说明
x Ref 当前X轴位置
y Ref 当前Y轴位置
isDragging Ref 是否正在拖拽
stop () => void 停止拖拽功能
start () => void 重新启用拖拽功能
applyTransform () => void 立即应用当前transform样式

注意事项:

  1. 目标元素需要设置为 position: fixedposition: absolute
  2. 边界计算需要考虑元素自身的尺寸
  3. 使用transform实现位移,不会影响文档流布局
  4. 触摸事件需要正确处理多点触控场景

该实现提供了与VueUse相似的拖拽功能,同时包含以下增强特性:

  • 更严格的边界限制处理
  • 更完善的触摸事件支持
  • 更直观的API设计
  • 更好的TypeScript类型提示
相关推荐
2301_764441332 分钟前
小说文本分析工具:基于streamlit实现的文本分析
前端·python·信息可视化·数据分析·nlp
jackl的科研日常16 分钟前
“个人陈述“的“十要“和“十不要“
前端
一个处女座的程序猿O(∩_∩)O20 分钟前
Vue 中 this 使用指南与注意事项
前端·javascript·vue.js
大有数据可视化1 小时前
数字孪生像魔镜,映照出无限可能的未来
前端·html·webgl
一个处女座的程序猿O(∩_∩)O1 小时前
使用 Docker 部署前端项目全攻略
前端·docker·容器
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_10空状态的固定表头表格
前端·javascript·vue.js·ecmascript·deepseek
天马37981 小时前
Vue 概念、历史、发展和Vue简介
前端·javascript·vue.js
小小鸭程序员1 小时前
NPM版本管理终极指南:掌握依赖控制与最佳实践
java·前端·spring·npm·node.js
KL's pig/猪头/爱心/猪头2 小时前
lws-minimal-ws-server前端分析
前端
TheK2 小时前
【源码分析】 一文搞清楚React全流程
前端