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>
  );
}

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

相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink4 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-6 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240259 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar9 小时前
纯前端实现更新检测
开发语言·前端·javascript