我在上次的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>
);
}
这样就有了开头的效果了。相当的完美是不是。