驾驭拖拽:从原生到vue和react hooks的实现技术探究

拖拽功能在今天的前端开发中变得非常普遍,不论是原生JavaScript还是流行的前端框架如Vue和React,都提供了丰富的拖拽实现方式。本文将带深入探讨拖拽技术的实现原理和不同框架下的实际应用,帮助大家全面掌握拖拽功能的实现方法。

原生

需求1:弹出拖拽框案例

  1. 点击拖拽按钮,显示拖拽框(阻止事件冒泡)
  2. 点击关闭按钮,隐藏拖拽框
  3. 点击页面的其他部分,隐藏拖拽框
  4. 点击拖拽框(阻止事件冒泡)
  5. 点击关闭,隐藏拖拽框
  6. 用户点击esc键之后,关闭拖拽框

需求2:拖拽

  1. 拖拽头部注册鼠标按下事件

  2. document注册鼠标移动事件(注意这里是document

    1. 我们知道鼠标距离屏幕左侧和顶部的距离, 拖拽框跟着鼠标移动,需要用鼠标距离屏幕 - 鼠标距离拖拽框的距离,所以当鼠标按下去的时候,先要计算出鼠标距离拖拽框的距离x,y

    2. 当鼠标拖动的时候,鼠标距离屏幕的距离 - x = drag需要移动的距离

    3. 控制屏幕弹框在可视区域内

      左边:最小0,右边:最大为屏幕宽度 - 盒子宽度 - 关闭弹框一半宽度

      上边:最小关闭弹框一半宽度,下边:屏幕高度 - 盒子宽度

  1. document注册鼠标抬起事件,并且移除document的鼠标移动事件

直接上代码吧

html 复制代码
<!DOCTYPE html>
<html>
​
<head lang="en">
  <meta charset="UTF-8" />
  <title></title>
  <style>
    * {
      padding: 0px;
      margin: 0px;
    }
​
    .drag {
      width: 512px;
      border: #ebebeb solid 1px;
      position: fixed;
      left: 0;
      top: 0;
      box-shadow: 0px 0px 20px #ddd;
      z-index: 2;
      background-color: white;
      display: none;
    }
​
    .drag-title {
      height: 52px;
      line-height: 52px;
      text-align: center;
      font-size: 20px;
      border-bottom: #ebebeb solid 1px;
      cursor: move;
      /* 禁止用户选择文字 */
      user-select: none;
    }
​
    .drag-content {
      padding: 12px;
      text-align: center;
    }
​
    .drag-content p {
      height: 36px;
      line-height: 36px;
    }
​
    .drag-close {
      width: 36px;
      height: 36px;
      line-height: 36px;
      text-align: center;
      border-radius: 50%;
      border: 1px solid #ebebeb;
      position: absolute;
      right: -18px;
      top: -18px;
      background-color: white;
      font-size: 12px;
      cursor: pointer;
    }
​
    .popup-draggable {
      height: 52px;
      line-height: 52px;
      text-align: center;
    }
​
    a {
      text-decoration: none;
      color: #000000;
    }
  </style>
</head>
​
<body>
  <div class="popup-draggable">
    <a id="link" href="javascript:void(0);">点击,弹出拖拽框</a>
  </div>
​
  <div class="drag">
    <div class="drag-title">
      请拖拽我
    </div>
    <div class="drag-content">
      <p>需求:弹出拖拽框案例</p>
      <p>1、点击拖拽按钮,显示拖拽框(阻止事件冒泡)</p>
      <p>2、点击关闭按钮,隐藏拖拽框</p>
      <p>3、点击页面的其他部分,隐藏拖拽框</p>
      <p>4、点击拖拽框(阻止事件冒泡)</p>
      <p>5、点击关闭,隐藏拖拽框</p>
      <p>6、用户点击esc键之后,关闭拖拽框</p>
    </div>
    <div class="drag-close">
      关闭
    </div>
  </div>
​
  <script>
    /*
      需求1:弹出拖拽框案例
      1、点击拖拽按钮,显示拖拽框(阻止事件冒泡)
      2、点击关闭按钮,隐藏拖拽框
      3、点击页面的其他部分,隐藏拖拽框
      4、点击拖拽框(阻止事件冒泡)
      5、点击关闭,隐藏拖拽框
      6、用户点击esc键之后,关闭拖拽框
     */
    const link = document.querySelector('#link')
    const drag = document.querySelector('.drag')
    const close = document.querySelector('.drag-close')
    const dragTitle = document.querySelector('.drag-title')
    link.addEventListener('click', function (e) {
      e.stopPropagation()
      drag.style.display = 'block'
      drag.style.top = (window.innerHeight - drag.offsetHeight) / 2 + 'px'
      drag.style.left = (window.innerWidth - drag.offsetWidth) / 2 + 'px'
    })
    document.addEventListener('click', function (e) {
      drag.style.display = 'none'
    })
    drag.addEventListener('click', function (e) {
      e.stopPropagation()
    })
    close.addEventListener('click', function (e) {
      drag.style.display = 'none'
    })
    document.addEventListener('keyup', function (e) {
      if (e.keyCode !== 27) return
      drag.style.display = 'none'
    })
    /*
    需求2:拖拽
    1、拖拽头部注册鼠标按下事件
    2、document注册鼠标移动事件(注意这里是document)
    */
    let x = 0
    let y = 0
​
    dragTitle.addEventListener('mousedown', dragFn)
​
    function dragFn(e) {
      console.log('按下鼠标', e.pageX, e.pageY)
      // 2.1 我们知道鼠标距离屏幕左侧和顶部的距离,
      // 拖拽框跟着鼠标移动,需要用鼠标距离屏幕 - 鼠标距离拖拽框的距离
      // 所以当鼠标按下去的时候,先要计算出鼠标距离拖拽框的距离
      x = e.pageX - drag.offsetLeft
      y = e.pageY - drag.offsetTop
      // 2.2 给document注册移动事件
      document.addEventListener('mousemove', moveFn)
    }
​
    function moveFn(e) {
      console.log('拖动鼠标')
      // 2.3 鼠标距离屏幕的距离 - x = drag需要移动的距离
      let moveX = e.pageX - x
      let moveY = e.pageY - y
      // 2.4 控制屏幕弹框在可视区域内
      // 左边:最小0,右边:最大为屏幕宽度 - 盒子宽度 - 关闭弹框一半宽度
      // 上边:最小关闭弹框一半宽度,下边:屏幕高度 - 盒子宽度
      const maxWidth = window.innerWidth - drag.offsetWidth - 18
      moveX = Math.min(moveX, maxWidth)
      moveX = Math.max(0, moveX)
      const maxHeight = window.innerHeight - drag.offsetHeight
      moveY = Math.min(moveY, maxHeight)
      moveY = Math.max(18, moveY)
      drag.style.left = moveX + 'px'
      drag.style.top = moveY + 'px'
    }
    // 特别注意:这里是给document添加抬起事件
    document.addEventListener('mouseup', function (e) {
      console.log('鼠标弹起了')
      // 特别注意:移除的是document的mousemove事件
      document.removeEventListener('mousemove', moveFn)
    })
  </script>
</body>
​
</html>

vue hooks

新建hooks文件

src文件下新建hooks文件夹,新建文件useDraggable.ts

typescript 复制代码
const useDraggable = (x = 0, y = 0) => {
  const position = reactive({x, y})
  const box = ref<HTMLDivElement | null>(null)
  const handleMouseDown = (e: MouseEvent) => {
    const startX = e.clientX - position.x
    const startY = e.clientY - position.y

    const handleMouseMove = (e: MouseEvent) => {
      let distanceX = e.clientX - startX
      let distanceY = e.clientY - startY
      const maxX = document.body.clientWidth - box.value!.offsetWidth
      const maxY = document.body.clientHeight - box.value!.offsetHeight

      distanceX = Math.min(maxX, distanceX)
      distanceX = Math.max(0, distanceX)

      distanceY = Math.min(maxY, distanceY)
      distanceY = Math.max(0, distanceY)
      position.x = distanceX
      position.y = distanceY
    }

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove)
      document.removeEventListener('mouseup', handleMouseUp)
    }

    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
  }

  return {
    onMouseDown: handleMouseDown,
    box,
    position
  }
}

export default useDraggable

使用方式

vue 复制代码
<template>
  <div class="drag" ref="box" :style="dragPositionStyle" @mousedown="onMouseDown">
  </div>
</template>
<script lang="ts" setup>
import useDraggable from '@/hooks/useDraggable'
const { position, onMouseDown, box} = useDraggable()
const dragPositionStyle = computed(() => ({left: `${position.x}px`, top: `${position.y}px`}))
</script>

react hooks

新建hooks文件

src文件下新建hooks文件夹,新建文件useDraggable.ts

ini 复制代码
import { useRef, useState } from 'react';

const useDraggable = (x = 0, y = 0) => {
  const [position, setPosition] = useState({ x, y });
  const box = useRef(null);
  const handleMouseDown = (e) => {
    const startX = e.clientX - position.x;
    const startY = e.clientY - position.y;

    const handleMouseMove = (e) => {
      let distanceX = e.clientX - startX;
      let distanceY = e.clientY - startY;

      let maxX = document.body.clientWidth - box.current.offsetWidth;
      let maxY = document.body.clientHeight - box.current.offsetHeight;

      distanceX = Math.min(maxX, distanceX);
      distanceX = Math.max(0, distanceX);

      distanceY = Math.min(maxY, distanceY);
      distanceY = Math.max(0, distanceY);
      setPosition({
        x: distanceX,
        y: distanceY,
      });
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  return {
    style: {
      position: 'fixed',
      left: position.x + 'px',
      top: position.y + 'px',
      cursor: 'move',
    },
    onMouseDown: handleMouseDown,
    box
  };
};

export default useDraggable;

使用方式

javascript 复制代码
import useDraggable from '@/hooks/useDraggable';
const { style, onMouseDown, box } = useDraggable(window.innerWidth - 160, window.innerHeight / 2 - 100);
<div style={style} onMouseDown={onMouseDown} ref={box}></div>

vuereacthooks略有不同,大家可以在此基础扩展功能和细节!

相关推荐
coder阿龙1 分钟前
【UNIAPP】获取视频的第一帧作为封面(基于视频URL,Canvas)复制即用
前端·uni-app·音视频
蘑菇王3 分钟前
无需打包构建?ESM Bundleless 开发的探索与实践
前端·javascript
只会写Bug的程序员7 分钟前
面试之《TypeScript泛型》
前端·面试·typescript
宇寒风暖17 分钟前
HTML嵌入CSS样式超详解(尊享)
前端·css·笔记·学习·html
秋天的一阵风22 分钟前
‌ES Module 都过十岁生日了,你还不了解它的运行原理吗?
前端·javascript·面试
FreeCultureBoy25 分钟前
本地运行LLM的实用指南
前端
二川bro43 分钟前
前端项目Axios封装Vue3详细教程(附源码)
前端
古柳_Deserts_X44 分钟前
看看 ManusAI 相关网站长啥样。通过「新词新站」思路挖到720K月访问、140K月访问的两个新站
前端·程序员·创业
Moment1 小时前
前端白屏检测SDK:从方案设计到原理实现的全方位讲解 ☺️☺️☺️
前端·javascript·面试
阿波次嘚1 小时前
关于在electron(Nodejs)中使用 Napi 的简单记录
前端·javascript·electron