React 模态框的设计(三)拖动组件的完善

我在上次的Draggable组件的设计中给了一个简化的方法,今天我来完善一下这个组件,可用于任何可移动组件的包裹。完善后的效果如下所示:

这个优化中,增加了一个注目的效果,还增加了触发可拖动区域的指定功能,这样我们对可拖动组件有更大的自由掌控。

Draggable 中增加以下两个Props

  • enableHandler 是否启用可拖动句柄

  • draggableHandler 可拖动句柄字符

上面的draggableHandler就是一个类名,如果 enableHandlertrue的情况下,可拖动组件中含有 draggableHandler 指定的类名的子组件的 mouseDown 事件会触发拖动的移动效果。其实触发事件是通过Draggable代理实现的。

javascript 复制代码
import React, { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';

/**
 * 拖动组件
 * @param {是否启用拖动句柄 } enableHandler 
 * @param {拖动句柄的类名} draggableHandler
 */
export default function Draggable({
    children, // 子组件
    enableHandler = false, // 是否启用拖动句柄
    draggableHandler = ".modelHandler" // 拖动句柄的类名
}) {
    const [isDragging, setIsDragging] = useState(false); // 是否正在拖动
    const [canDrag, setCanDrag] = useState(!enableHandler); // 是否可以拖动
    const [position, setPosition] = useState({ x: 0, y: 0 }); // 位置
    const offsetX = useRef(0); // x轴偏移量
    const offsetY = useRef(0); // y轴偏移量

    useEffect(() => {
        // 鼠标移动事件
        const handleMouseMove = (e) => {
            if (isDragging) {
                setPosition({
                    x: e.clientX - offsetX.current,
                    y: e.clientY - offsetY.current
                });
            }
        };

        // 鼠标抬起事件
        const handleMouseUp = (e) => {
            if(e.button !== 0) return;
            setIsDragging(false);
        };

        // 在相关的事件委托到document上
        if (isDragging) {
            document.addEventListener('mousemove', handleMouseMove);
            document.addEventListener('mouseup', handleMouseUp);
        } else {
            document.removeEventListener('mousemove', handleMouseMove);
            document.removeEventListener('mouseup', handleMouseUp);
        }

        // 组件卸载时移除事件
        return () => {
            document.removeEventListener('mousemove', handleMouseMove);
            document.removeEventListener('mouseup', handleMouseUp);
        };
    }, [isDragging]);

    const onMouseMove = (e) => {
        if (enableHandler) {
            // 判断是否在拖动句柄上
            if (document.elementFromPoint(e.clientX, e.clientY).className.includes(draggableHandler)) {
                setCanDrag(true);
            } else {
                setCanDrag(false);
            }
        }
        
    }

    const handleMouseDown = (e) => {
        e.preventDefault();
        e.stopPropagation();

        if (enableHandler) {
            // 判断是否在拖动句柄上
            if (document.elementFromPoint(e.clientX, e.clientY).className.includes(draggableHandler)) {
                if (e.button !== 0) return;
                setIsDragging(true);
                offsetX.current = e.clientX - position.x;
                offsetY.current = e.clientY - position.y;
            }
        } else {
            if (e.button !== 0) return;
            setIsDragging(true);
            offsetX.current = e.clientX - position.x;
            offsetY.current = e.clientY - position.y;
        }
    };

    return (
        <Box
            sx={{
                position: 'relative',
                transform: `translate(${position.x}px, ${position.y}px)`,
                cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",
                transition: `transform:`,
            }}
            onMouseDown={handleMouseDown}
            onMouseMove={onMouseMove}
        >
            <Box
                sx={{
                    transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,
                    transition: `transform 200ms ease-in-out`,
            }}>
                {
                    children
                }
            </Box>
        </Box>
    );
}

上面的逻辑并不复杂,通过过下面的语句可以找到含有指定类名的组件:

javascript 复制代码
document.elementFromPoint(e.clientX, e.clientY).className.includes(draggableHandler)

这样就能判断当前鼠标是否处于指定的组件上并启动移动效果。 由于我们要实现抓取注目动画和移动动画,都是通过 transform实现的,但是我们只要缩放动画,所以我用了两层Box包裹来分割transform属性。

为了测试这个Draggable, 我来做个小组件测试 draggableHandler 的作用。

javascript 复制代码
/** @jsxImportSource @emotion/react */
import { css, jsx } from '@emotion/react'
import Box from '@mui/material/Box';

const titleBarCss = css`
        background-color: #BDBDBD;
        padding: 8px;
    `;

const modelContentCss = css`
        padding: 16px;
    `;

const ModalTest = ({width = 400, height = 300, bgColor = "white"}) => {
    return (
        <Box css={css`
        position: relative;
        background-color: ${bgColor};
        border: 1px solid #ccc;
        border-radius: 5px;
        overflow: hidden;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        width: ${width}px;
        height: ${height}px;
    `}>
            <Box
                css={titleBarCss}
                className=".modelHandler"
            >
                这是标题
            </Box>
            <Box css={modelContentCss}>
                这是弹窗内容
            </Box>
        </Box>
    );
};

export default ModalTest;

上面的组件中,我们在标题栏上增加了类名: modelHandler。很简单是不是。接下来我们来完整的测试:

javascript 复制代码
import React from "react";
import Stack from "@mui/material/Stack";
import Draggable from "../../framework-kakaer/SModel/_Draggable";
import ModelTest from "../../framework-kakaer/SModel/_DraggableContent";

export default function DraggableTest() {
  return (
      <Stack spacing={3}>
          <Stack direction="row" spacing={2}>
              <Draggable>
                  <div style={{ width: 200, height: 200, backgroundColor: 'red' }}>Draggable</div>
              </Draggable>

              <Draggable>
                  <div style={{ width: 200, height: 200, backgroundColor: 'green' }}>Draggable</div>
              </Draggable>

              <Draggable>
                  <div style={{ width: 200, height: 200, backgroundColor: 'blue' }}>Draggable</div>
              </Draggable>
          </Stack>

          <Stack direction="row" spacing={2}>
              <Draggable enableHandler={true}>
                  <ModelTest width={200} height={200} bgColor="yellow" />
              </Draggable>

              <Draggable>
                  <ModelTest width={200} height={200} bgColor="#FF9500" />
              </Draggable>

              <Draggable>
                  <ModelTest width={200} height={200} bgColor="#5AC8FA" />
              </Draggable>
          </Stack>
    </Stack>
  );
}

这样就有了开头的效果了。相当的完美是不是。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax