目录

手写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类型提示
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
孩子 你要相信光3 小时前
安卓edge://inspect 和 chrome://inspect调试移动设备上的网页
前端
乐闻x4 小时前
提升 React 应用性能:使用 React Profiler 进行性能调优
前端·javascript·react.js·性能优化
NaZiMeKiY6 小时前
HTML5前端第二章节
前端·html·html5
天若有情6736 小时前
深入浅出:HTML 中 <a> 标签嵌入链接教程
前端·html
烂蜻蜓6 小时前
HTML 样式之 CSS 全面解析
前端·css·html
冬冬小圆帽6 小时前
Webpack 优化深度解析:从构建性能到输出优化的全面指南
前端·webpack·node.js
大龄大专大前端8 小时前
JavaScript闭包的认识/应用/原理
前端·javascript·ecmascript 6
字节源流8 小时前
【SpringMVC】常用注解:@SessionAttributes
java·服务器·前端
肥肠可耐的西西公主8 小时前
前端(vue)学习笔记(CLASS 4):组件组成部分与通信
前端·vue.js·学习
烛阴8 小时前
JavaScript 函数绑定:从入门到精通,解锁你的代码超能力!
前端·javascript