React 模态框的设计(八)优化补充

文章介绍了在Draggable.jsx和ModelContainer.jsx中对弹窗组件的优化,主要涉及将onPointerEnter事件替换为useLayoutEffect,以提高动画性能。同时,两个组件中处理了拖动、位置变化和外部点击事件,确保了组件的交互性和响应性。
摘要由CSDN通过智能技术生成

在之前的弹窗的设计中,有两处地方现在做一点小小的优化,就是把_Draggable.jsx中的 onPointerEnter 事件 用 useLayoutEffect来规换,效果更佳,同样的,在_ModelContainer.jsx中也是一样。如下所示:
_Draggable.jsx

/** @jsxImportSource @emotion/react */
import { css, keyframes } from '@emotion/react'
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import { useOutsideClick } from './_useOutsideClick';
import { useWindowSize } from './_useWindowSize';
import { minHeight, minWidth } from './_ModelConfigure';

//弹窗的动画
const attentionKeyframes = keyframes`
    from,to {
        transform: scale(1);
    }
    50% {
        transform: scale(1.03);
    }
`;

//弹窗的开始时动画
const anim = css`
    animation: ${attentionKeyframes} 400ms ease;
`;

//弹窗的结束时动画
const stopAnim = css`
    animation: null;
`;

const draggableHandler = ".model-handler"; // 拖动句柄的类名

/**
 * 拖动组件,使被包裹的组件可以拖动,支持拖动句柄
 * @param {是否启用拖动句柄 } enableHandler 
 * @param {拖动句柄的类名} draggableHandler
 * @param {外部点击事件} onOutsideClick
 */
export default function Draggable({
    children, // 子组件
    enableDragging = true,
    enableHandler = false, // 是否启用拖动句柄
    stateMode
}) {
    const [attentionStyle, setAttentionStyle] = useState(anim); // 弹窗动画,当点击外部时,弹窗会有一个动画效果
    const [isDragging, setIsDragging] = useState(false); // 是否正在拖动
    const [canDrag, setCanDrag] = useState(true); // 是否可以触发拖动操作,改变鼠标样式

    const normalPos = useRef({ x: 0, y: 0 }); // 正常模式下弹窗的位置(translate的值)
    const minPos = useRef({ x: 0, y: 0 }); // 最小化时的位置
    const maxPos = { x: 0, y: 0 }; // 最大化时的位置,因为最大化时弹窗的位置是固定的,所以不需要ref

    // 当所有模式下的位置变化都是通过position来反映到UI上的,所以position是唯一的位置状态
    const [position, setPosition] = useState({x: 0, y: 0}); // 弹窗的位置(translate的值)

    // 当鼠标按下时,记录鼠标的位置并以当前位置为基准进行拖动(相对位置),与position的差值为偏移量,position为上一次的偏移量。
    // 因为采用的是translate的方式进行拖动,这种方式下,是以组件第一次渲染的位置为基准参考点(也就是相对0,0的位置)进行拖动的.
    // 正常模式下的偏移量
    const normalOffsetX = useRef(0); // x轴偏移量
    const normalOffsetY = useRef(0); // y轴偏移量

    // 最小化时的偏移量
    const minOffsetX = useRef(0); // x轴偏移量
    const minOffsetY = useRef(0); // y轴偏移量

    const initedRect = useRef(0); // 初始化后的弹窗大小

    const wrapperRef = useRef(null);

    const windowSize = useWindowSize();

    // 当点击外部时,弹窗会有一个注目动画效果
    useOutsideClick(wrapperRef, () => {
        setAttentionStyle(anim);
    });

    // 弹窗注目动画的监听
    useEffect(function () {
        // 弹窗动画监听事件
        const listener = (e) => {
            if (e.type === "animationend") {
                setAttentionStyle(stopAnim);
            }
        };

        if (wrapperRef.current !== null) {
            wrapperRef.current.addEventListener("animationend", listener, true);
        }

        return () => {
            if (wrapperRef.current !== null) {
                wrapperRef.current.removeEventListener("animationend", listener);
            }
        };
    }, []);

    // document的鼠标移动事件和鼠标抬起事件监听
    useEffect(() => {
        // 鼠标移动事件
        const handleMouseMove = (e) => {
            if (isDragging) {
                switch (stateMode) {
                    case 0:
                        const xt = e.clientX - minOffsetX.current;
                        const yt = e.clientY - minOffsetY.current;
                        const xtMinTop = -((windowSize.height - minHeight) / 2 - 10);
                        const xtMaxTop = (windowSize.height - minHeight) / 2 - 10;
                        const xtMinLeft = -((windowSize.width - minWidth) / 2 - 10);
                        const xtMaxLeft = (windowSize.width - minWidth) / 2 - 10;
                        const xm = xt < xtMinLeft ? xtMinLeft : xt > xtMaxLeft ? xtMaxLeft : xt;
                        const ym = yt < xtMinTop ? xtMinTop : yt > xtMaxTop ? xtMaxTop : yt;
                        minPos.current = { x: xm, y: ym};
                        setPosition({ ...minPos.current });
                        break;
                    
                    case 2:
                        break;
                    default:
                        const xTmp = e.clientX - normalOffsetX.current;
                        const yTmp = e.clientY - normalOffsetY.current;
                        const minLetf = -(windowSize.width - initedRect.current.width) / 2; 
                        const minTop = -(windowSize.height - initedRect.current.height) / 2;
                        const maxLeft = (windowSize.width - initedRect.current.width) / 2;
                        const maxTop = (windowSize.height - initedRect.current.height) / 2;
                        const x = xTmp < minLetf ? minLetf : xTmp > maxLeft ? maxLeft : xTmp;
                        const y = yTmp < minTop ? minTop : yTmp > maxTop ? maxTop : yTmp;
                        normalPos.current = { x, y };
                        setPosition({ ...normalPos.current });
                        break;
                }
            }
        };

        // 鼠标抬起事件
        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]);

    // 弹窗位置的监听, 每当弹窗状态改变时,都会重新设置弹窗的位置, 将相应状态下的最后位置设置为当前位置
    // 但最小化状态下的位置有所不同,因为最小化状态下的初始位置为左下角,每次从其它状态切换到最小化状态时都要进行相同的设置。
    useEffect(() => {
        switch (stateMode) {
            case 0:
                const initX = -((windowSize.width - minWidth - 20) / 2);
                const initY = windowSize.height / 2 - minHeight + 10;
                setPosition({ x: initX, y: initY });
                minPos.current = { x: initX, y: initY };
                break;
            case 2:
                setPosition({...maxPos.current});
                break;
            default:
                setPosition({ ...normalPos.current });
                break;
        }
    }, [stateMode]);

    // ref对象的鼠标移动事件,用于判断是否在拖动句柄上
    const onMouseMove = (e) => {
        if (!enableDragging) {
            setCanDrag(false);
            return;
        }
            
        if (enableHandler) {
            const clickedElement = e.target;
            // 检查鼠标点击的 DOM 元素是否包含特定类名
            if (clickedElement.classList.contains(draggableHandler)) {
                setCanDrag(true);
            } else {
                setCanDrag(false);
            }
        }
    }

    // ref对象的鼠标按下事件,用于触发拖动操作,
    // 如果启用了拖动句柄,那么只有在拖动句柄上按下鼠标才会触发拖动操作,
    // 否则直接按下鼠标就会触发拖动操作
    const handleMouseDown = (e) => {
        if (!enableDragging) return;
        switch (stateMode) {
            case 0:
                if (enableHandler) {
                    // 判断是否在拖动句柄上
                    const curElement = e.target;

                    // 检查鼠标点击的 DOM 元素是否包含特定类名
                    if (curElement.classList.contains(draggableHandler)) {
                        if (e.button !== 0) return;
                        setIsDragging(true);
                        minOffsetX.current = e.clientX - minPos.current.x;
                        minOffsetY.current = e.clientY - minPos.current.y;
                    } else {
                        setCanDrag(false);
                    }
                } else {
                    if (e.button !== 0) return;
                    setIsDragging(true);
                    minOffsetX.current = e.clientX - minPos.current.x;
                    minOffsetY.current = e.clientY - minPos.current.y;
                }
                return;
            case 2:
                return; 
            default:
                if (enableHandler) {
                    // 判断是否在拖动句柄上
                    const curElement = e.target;

                    // 检查鼠标点击的 DOM 元素是否包含特定类名
                    if (curElement.classList.contains(draggableHandler)) {
                        if (e.button !== 0) return;
                        setIsDragging(true);
                        normalOffsetX.current = e.clientX - normalPos.current.x;
                        normalOffsetY.current = e.clientY - normalPos.current.y;
                    } else {
                        setCanDrag(false);
                    }
                } else {
                    if (e.button !== 0) return;
                    setIsDragging(true);
                    normalOffsetX.current = e.clientX - normalPos.current.x;
                    normalOffsetY.current = e.clientY - normalPos.current.y;
                }
                return;
        }
    };

    // 初始化时获取弹窗的大小,此方法替代了onPointerEnter事件,效果要好一些
    useLayoutEffect(() => {
        if (wrapperRef.current) {
            const rect = wrapperRef.current.getBoundingClientRect();
            initedRect.current = {
                width: rect.width,
                height: rect.height,
            };
        }
    }, []);

    return (
        <Box
            ref={wrapperRef}
            sx={{
                transform: `translate(${position.x}px, ${position.y}px)`,
                cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",
                transition: isDragging ? null : `transform 200ms ease-in-out`,
            }}
            onMouseDown={handleMouseDown}
            onMouseMove={onMouseMove}
            onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
            // onPointerEnter={() => {
            //     if (initedRect.current === 0 && wrapperRef.current !== null) {
            //         const rect = wrapperRef.current.getBoundingClientRect();
            //         initedRect.current = {
            //             width: rect.width,
            //             height: rect.height,
            //         };
            //     }
            // }}
        >
            <Box
                sx={{
                    transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,
                    transition: `transform 200ms ease-in-out`,
                }}
                css={attentionStyle}
            >
                {
                    children
                }
            </Box>
        </Box>
    );
}

_ModelContainer.jsx中做如下更改:
_ModelContainer.jsx

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useLayoutEffect, useRef, useState } from 'react';
import { Paper } from '@mui/material';
import { useModelState } from './useModel';
import { infoLevel } from './_ModelConfigure';

// 计算不同状态下的高度
const calHeight = (sizeMode, normalHeight) => {
    switch (sizeMode) {
        case 0:
            return '45px';
        case 1:
            return normalHeight > 0 ? normalHeight + 'px' : 'auto';
        case 2:
            return '100vh';
        default:
            return 'auto';
    }
}

// 最大化时的固定样式
const maxSizeCss = css`
        width: 100vw;
        height: 100vh;
        top: 0;
        left: 0;
    `;

/**
 * 弹窗容器
 * @param {*} param0 
 * @returns 
 */
const ModelContainer = ({ children }) => {
    const modelState = useModelState();
    const {
        stateMode, // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
        level, // 弹窗的类型(主要是颜色类型),选项有:default, error, warning, success, info
        isDark, //是否是暗黑模式 
    } = modelState;

    const [nomalSize, setNormalSize] = useState({ width: 0, height: 0 });
    const containerRef = useRef(null);

    useLayoutEffect(() => {
        if (containerRef.current !== null) {
            const rect = containerRef.current.getBoundingClientRect();
            setNormalSize({
                width: rect.width,
                height: rect.height,
            });
        }
    }, []);

    return (
        <Paper
            ref={containerRef}
            css={css`
                border: 1px solid #A0A0A0;
                border-radius: 5px;
                width: ${ stateMode === 2 ? '100vw' : stateMode === 0 ? '300px' : '576px' };
                height: ${ calHeight(stateMode, nomalSize.height) };
                overflow: hidden;
                max-width: 100%;
                max-height: 100vh;
                display: flex;
                flex-direction: column;
                background-color: ${isDark ? '#333' : infoLevel[level] ? infoLevel[level].color :  "white" };
                ${stateMode === 2 ? maxSizeCss : null};
                transition: all 0.3s;
            `}
        >
            {
                children
            }
        </Paper>
    );
};

export default ModelContainer;

以上两点做个补充。

  • 30
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码蚁先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值