本章代码结构: 主入口Test.tsx , 组件:ResizeControl.tsx
本章花费俩天时间完成代码例子, 单独抽离代码 封装好一个 ResizeControl 组件, 拿来即用。
代码中
const domObj = document.getElementById(
resize-item-${startPos.id})
这句是关键代码, 不然获取的dom节点有问题,导致多个红色div操作时候会重叠
- ResizeControl.tsx
csharp
/* eslint-disable no-case-declarations */
import { FC, useEffect, useRef, useState } from 'react';
import styles from './index.module.scss';
import type { Demo } from '../../pages/Test';
interface PropsType {
children: JSX.Element | JSX.Element[];
value: Demo;
emitData: (val: Demo) => void;
}
// 获取旋转角度参数
function getRotate(transform: string) {
// 假设 transform 是 "rotate(45deg)"
// console.info('transform', transform);
if (!transform) return 0;
const match = /rotate\(([^)]+)\)/.exec(transform);
const rotate = match ? parseFloat(match[1]) : 0;
// console.info(890, rotate);
return rotate;
}
const ResizeControl: FC<PropsType> = (props: PropsType) => {
const { children, value, emitData } = props;
const points = ['lt', 'tc', 'rt', 'rc', 'br', 'bc', 'bl', 'lc'];
const [startPos, setStartPos] = useState<Demo>(value);
const resizeItemRef = useRef(null);
const isDown = useRef(false);
const [direction, setDirection] = useState('');
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [isDown, startPos]);
// 鼠标被按下
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
console.info('onMouseDown', e.currentTarget);
e.stopPropagation();
// e.preventDefault();
// 获取 整个 resize-item 的dom节点
const domObj = document.getElementById(`resize-item-${startPos.id}`);
if (!domObj) return;
resizeItemRef.current = domObj;
if (!resizeItemRef.current) return;
const { width, height, transform } = resizeItemRef.current.style;
// 获取当前操作 dom data-key
const direction = e.currentTarget.getAttribute('data-key') || '';
console.log('元素方向', direction);
setDirection(direction);
// 获取旋转角度
const rotate = getRotate(transform);
// 记录状态
isDown.current = true;
setStartPos({
...startPos,
startX: e.clientX,
startY: e.clientY,
width: +width.replace(/px/, '') - 2,
height: +height.replace(/px/, '') - 2,
rotate,
});
};
const handleMouseMove = (e: { clientX: number; clientY: number }) => {
if (isDown.current && resizeItemRef.current) {
const { rotate, startX, startY } = startPos;
let { height, width, left, top } = startPos;
// console.log('startPos', startPos);
const curX = e.clientX;
const curY = e.clientY;
// 计算偏移量
const offsetX = curX - startX;
const offsetY = curY - startY;
// console.info('offsetX', offsetX, offsetY);
const rect = resizeItemRef.current.getBoundingClientRect();
let nowRotate = 0;
switch (direction) {
// 右中
case 'rc':
width += offsetX;
break;
// 左中
case 'lc':
width -= offsetX;
left += offsetX;
break;
// 底中
case 'bc':
height += offsetY;
break;
// 顶中
case 'tc':
height -= offsetY;
top += offsetY;
break;
// 右上角
case 'rt':
height -= offsetY;
top += offsetY;
width += offsetX;
break;
// 左上角
case 'lt':
height -= offsetY;
top += offsetY;
width -= offsetX;
left += offsetX;
break;
// 右下角
case 'br':
height += offsetY;
width += offsetX;
break;
// 左下角
case 'bl':
height += offsetY;
width -= offsetX;
left += offsetX;
break;
case 'rotate':
// 获取元素中心点位置
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// 旋转前的角度
const rotateDegreeBefore =
Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180);
// 旋转后的角度
const rotateDegreeAfter =
Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180);
// 获取旋转的角度值
nowRotate = rotateDegreeAfter - rotateDegreeBefore + rotate;
resizeItemRef.current.style.transform = `rotate(${nowRotate}deg)`;
break;
case 'move':
left += offsetX;
top += offsetY;
// 获取父元素的边界 (打开注释, 可验证边界条件判断)
// const parent = resizeItemRef.current.parentElement;
// if (!parent) return;
// const parentRect = parent.getBoundingClientRect();
// // 限制div不超过父元素的边界
// const maxTop = parentRect.height - height;
// const maxLeft = parentRect.width - width;
// left = Math.min(Math.max(left, 0), maxLeft);
// top = Math.min(Math.max(top, 0), maxTop);
break;
}
// console.log('-----', width, height);
resizeItemRef.current.style.width = width + 'px';
resizeItemRef.current.style.height = height + 'px';
resizeItemRef.current.style.left = left + 'px';
resizeItemRef.current.style.top = top + 'px';
const newPos = {
...startPos,
height,
width,
startX: curX,
startY: curY,
left,
top,
rotate: nowRotate,
};
emitData(newPos);
setStartPos(newPos);
}
};
if (!children) return null;
const handleMouseUp = () => {
console.info('clear。。。。');
isDown.current = false;
resizeItemRef.current = null;
};
return (
<div
className={styles['resize-item']}
style={{
left: `${startPos.left}px`,
top: `${startPos.top}px`,
width: `${startPos.width + 2}px`,
height: `${startPos.height + 2}px`,
}}
id={`resize-item-${startPos.id}`}
>
{points.map((item, index) => (
<div
key={index}
data-key={item}
onMouseDown={onMouseDown}
className={[
styles['resize-control-btn'],
styles[`resize-control-${item}`],
].join(' ')}
></div>
))}
<div
className={styles['resize-control-rotator']}
onMouseDown={onMouseDown}
data-key={'rotate'}
>
转
</div>
<div
data-key={'move'}
onMouseDown={(e) => onMouseDown(e)}
style={{ width: '100%', height: '100%', background: 'yellow' }}
>
{/* <span style={{ wordBreak: 'break-all', fontSize: '10px' }}>
{JSON.stringify(startPos)}
</span> */}
{children}
</div>
</div>
);
};
export default ResizeControl;
- ResizeControl/index.module.scss
csharp
.resize-item {
cursor: move;
position: absolute;
z-index: 2;
border: 1px dashed yellow;
box-sizing: border-box;
}
$width_height: 4px; // 建议偶数
.resize-control-btn {
position: absolute;
width: $width_height;
height: $width_height;
background: yellow;
// user-select: none; // 注意禁止鼠标选中控制点元素,不然拖拽事件可能会因此被中断
z-index: 1;
}
.resize-control-btn.resize-control-lt {
cursor: nw-resize;
top: $width_height / -2;
left: $width_height / -2;
}
.resize-control-btn.resize-control-tc {
cursor: ns-resize;
top: $width_height / -2;
left: 50%;
margin-left: $width_height / -2;
}
.resize-control-btn.resize-control-rt {
cursor: ne-resize;
top: $width_height / -2;
right: $width_height / -2;
}
.resize-control-btn.resize-control-rc {
cursor: ew-resize;
top: 50%;
margin-top: $width_height / -2;
right: $width_height / -2;
}
.resize-control-btn.resize-control-br {
cursor: se-resize;
bottom: $width_height / -2;
right: $width_height / -2;
}
.resize-control-btn.resize-control-bc {
cursor: ns-resize;
bottom: $width_height / -2;
left: 50%;
margin-left: $width_height / -2;
}
.resize-control-btn.resize-control-bl {
cursor: sw-resize;
bottom: $width_height / -2;
left: $width_height / -2;
}
.resize-control-btn.resize-control-lc {
cursor: ew-resize;
top: 50%;
margin-top: $width_height / -2;
left: $width_height / -2;
}
.resize-control-rotator {
position: absolute;
cursor: pointer;
bottom: -20px;
left: 50%;
margin-left: -10px;
width: 20px;
text-align: center;
font-size: 10px;
background: red;
}
- 主入口 Test.tsx
csharp
import React, { useState, DragEvent, useRef, useEffect } from 'react';
import ResizeControl from '../../components/ResizeControl';
export interface Demo {
id: number;
left: number;
top: number;
startX: number;
startY: number;
width: number;
height: number;
rotate: number;
}
// 获取元素旋转角度
function getDomRotate(transform: string) {
// 假设 transform 是 "rotate(45deg)"
// console.info('transform', transform);
const match = /rotate\(([^)]+)\)/.exec(transform);
const rotate = match ? parseFloat(match[1]) : 0;
// console.info(890, rotate);
return rotate;
}
const App: React.FC = () => {
const [demos, setDemos] = useState<Demo[]>([]);
const divRef = useRef<HTMLDivElement | null>(null);
const [activeDomId, setActiveDomId] = useState(0);
const handleDragStart = (e: DragEvent<HTMLDivElement>, id: number) => {
e.dataTransfer.setData('id', id.toString());
// 鼠标偏移量
const offsetX = e.clientX - e.currentTarget.getBoundingClientRect().left;
const offsetY = e.clientY - e.currentTarget.getBoundingClientRect().top;
e.dataTransfer.setData('offsetX', offsetX.toString());
e.dataTransfer.setData('offsetY', offsetY.toString());
divRef.current = e.currentTarget;
};
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
console.info('鼠标释放位置', e.clientX, e.clientY);
const contentDom = document.getElementById('content');
if (!contentDom) return;
const contentStyle = contentDom.getBoundingClientRect();
// console.info('contentStyle', contentStyle);
const { left, top } = contentStyle;
const offsetX = +e.dataTransfer.getData('offsetX') || 0;
const offsetY = +e.dataTransfer.getData('offsetY') || 0;
// console.info('offsetX', offsetX, offsetY);
const newLeft = e.clientX - left - offsetX;
const newTop = e.clientY - top - offsetY;
if (!divRef.current) return;
const { width, height, transform } = divRef.current.style;
// 元素旋转角度
let rotate = 0;
if (!transform) {
rotate = 0;
} else {
rotate = getDomRotate(transform);
}
const newDemo: Demo = {
id: e.dataTransfer.getData('id'),
startX: e.clientX,
startY: e.clientY,
left: newLeft,
top: newTop,
width: +width.replace(/px/, ''),
height: +height.replace(/px/, ''),
rotate,
};
setDemos([...demos, newDemo]);
};
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
};
return (
<div>
<div
id="demo"
draggable
onDragStart={(e) => handleDragStart(e, +new Date())}
style={{
width: '100px',
height: '100px',
backgroundColor: 'red',
margin: '30px',
cursor: 'pointer',
}}
>
demo2
</div>
<div
id="content"
onDrop={handleDrop}
onDragOver={handleDragOver}
style={{
width: '300px',
height: '300px',
margin: '30px',
backgroundColor: 'blue',
position: 'relative',
}}
>
{demos.map((demo) => (
<ResizeControl
key={demo.id}
value={demo}
emitData={(data) => {
setDemos((prevDemos) =>
prevDemos.map((a) => {
return a.id == data.id ? data : a;
})
);
}}
>
{/* 当前 div 组件 */}
<div
style={{
backgroundColor: 'red',
width: '100%',
height: '100%',
}}
>
{/* <span style={{ wordBreak: 'break-all', fontSize: '10px' }}>
{JSON.stringify(demo)}
</span> */}
</div>
</ResizeControl>
))}
</div>
</div>
);
};
export default App;