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

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

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

Draggable 中增加以下两个Props

  • enableHandler 是否启用可拖动句柄

  • draggableHandler 可拖动句柄字符

上面的draggableHandler就是一个类名,如果 enableHandler true 的情况下,可拖动组件中含有 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>
  );
}

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

相关推荐
疯狂的沙粒8 分钟前
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
前端·uni-app·html
小妖66612 分钟前
html 滚动条滚动过快会留下边框线
前端·html
heroboyluck26 分钟前
Svelte 核心语法详解:Vue/React 开发者如何快速上手?
前端·svelte
海的诗篇_27 分钟前
前端开发面试题总结-JavaScript篇(二)
开发语言·前端·javascript·typescript
琹箐37 分钟前
ant-design4.xx实现数字输入框; 某些输入法数字需要连续输入两次才显示
前端·javascript·anti-design-vue
程序员-小李38 分钟前
VuePress完美整合Toast消息提示
前端·javascript·vue.js
Uyker1 小时前
从零开始制作小程序简单概述
前端·微信小程序·小程序
Dontla5 小时前
为什么React列表项需要key?(React key)(稳定的唯一标识key有助于React虚拟DOM优化重绘大型列表)
javascript·react.js·ecmascript
EndingCoder6 小时前
React从基础入门到高级实战:React 实战项目 - 项目三:实时聊天应用
前端·react.js·架构·前端框架
阿阳微客7 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏