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 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫3 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦4 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子4 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享5 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
从兄6 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf7 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨7 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL7 小时前
npm入门教程1:npm简介
前端·npm·node.js