contant.ts
export const movePoints: MovePoint[] = [
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'middleTop',
'middleBottom',
'middleLeft',
'middleRight',
];
export type MovePoint =
| 'topLeft'
| 'topRight'
| 'middleLeft'
| 'middleRight'
| 'bottomLeft'
| 'bottomRight'
| 'middleTop'
| 'middleBottom';
export interface MoveBlock {
width: number;
height: number;
top: number;
left: number;
}
export interface DragResizeProps extends React.HTMLAttributes<'div'> {
canResize?: boolean; // 是否可缩放
active?: boolean; // 是否是激活状态
canMove?: boolean; // 是否可移动
keepRatio?: boolean; // 是否保持比率
confine?: boolean; // 是否限制在父元素内;
moveBlockInit?: MoveBlock; // 初始位置
onUpdate?: (data: MoveBlock) => any; // 更新数据时回调
}
export type UpdateBlock = Partial<MoveBlock>;
export interface Position {
x: number;
y: number;
}
export interface MoveEvent {
(diff: Position, start: MoveBlock): UpdateBlock;
}
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components/macro';
import {
DragResizeProps,
MoveBlock,
MoveEvent,
MovePoint,
movePoints,
UpdateBlock,
} from './contant';
const pointSize = 10;
const position = (pointSize / 2 + 0.5) * -1;
const dragColor = 'red';
const DragResizeContainer = styled.div`
position: relative;
.canResize {
&:hover {
border-color: ${dragColor};
.point {
transform: scale(1.2, 1.2);
transition: all 0.3s;
opacity: 1;
}
}
.point {
background: white;
z-index: 1;
}
.topLeft {
cursor: nw-resize;
}
.topRight {
cursor: ne-resize;
}
.middleTop {
cursor: n-resize;
}
.middleBottom {
cursor: s-resize;
}
.middleLeft {
cursor: w-resize;
}
.middleRight {
cursor: e-resize;
}
.bottomLeft {
cursor: sw-resize;
}
.bottomRight {
cursor: se-resize;
}
}
.container {
user-select: none; /* 不可复制 */
background-color: transparent;
position: absolute;
border: 1px dashed transparent;
.point {
opacity: 0; // 调试用
position: absolute;
width: ${pointSize}px;
height: ${pointSize}px;
border-radius: ${pointSize}px;
border: 1px solid ${dragColor};
background: white;
}
.topLeft {
top: ${position}px;
left: ${position}px;
}
.topRight {
top: ${position}px;
right: ${position}px;
}
.middleTop {
top: ${position}px;
left: 0;
right: 0;
margin: auto;
}
.middleBottom {
bottom: ${position}px;
left: 0;
right: 0;
margin: auto;
}
.middleLeft {
top: 0;
bottom: 0;
left: ${position}px;
margin: auto;
}
.middleRight {
top: 0;
bottom: 0;
right: ${position}px;
margin: auto;
}
.bottomLeft {
left: ${position}px;
bottom: ${position}px;
}
.bottomRight {
right: ${position}px;
bottom: ${position}px;
}
}
.active {
border-color: ${dragColor};
.point {
opacity: 1;
}
}
`;
const classNames = (className: object) => {
return Object.entries(className)
.map(([key, val]) => {
if (val) {
return key;
} else {
return '';
}
})
.join(' ');
};
const DragResize = (props: DragResizeProps) => {
const {
children,
canResize = true,
active = true,
canMove = true,
keepRatio,
confine,
onUpdate,
moveBlockInit,
} = props;
const [moveBlock, setMoveBlock] = useState<MoveBlock>();
const maxBlock = useRef({
width: 0,
height: 0,
});
const ratio = useRef<number>();
useEffect(() => {
if (moveBlockInit) {
setMoveBlock(moveBlockInit);
}
}, [moveBlockInit]);
useEffect(() => {
if (!keepRatio) ratio.current = undefined;
if (keepRatio && moveBlock && !ratio.current) {
if (moveBlock.width) {
ratio.current = moveBlock.height / moveBlock.width;
} else {
ratio.current = undefined;
}
}
}, [keepRatio, moveBlock]);
const prePosition = useRef<{
dragType?: MovePoint | 'block';
startX: number;
startY: number;
}>({
dragType: undefined,
startX: 0,
startY: 0,
});
const eventMap: Record<MovePoint, MoveEvent> = {
topLeft(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
width: start.width - dx,
left: start.left + dx,
height: start.height - dx * ratio.current,
top: start.top + dx * ratio.current,
};
}
return {
width: start.width - diff.x,
left: start.left + diff.x,
height: start.height - diff.y,
top: start.top + diff.y,
};
},
topRight(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
width: start.width + dx,
height: start.height + dx * ratio.current,
top: start.top - dx * ratio.current,
};
}
return {
width: start.width + diff.x,
height: start.height - diff.y,
top: start.top + diff.y,
};
},
bottomLeft(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
width: start.width - dx,
left: start.left + dx,
height: start.height - dx * ratio.current,
};
}
return {
width: start.width - diff.x,
height: start.height + diff.y,
left: start.left + diff.x,
};
},
bottomRight(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
width: start.width + dx,
height: start.height + dx * ratio.current,
};
}
return {
width: start.width + diff.x,
height: start.height + diff.y,
};
},
middleTop(diff, start) {
if (keepRatio && ratio.current) {
const dy = diff.y;
return {
height: start.height - dy,
top: start.top + dy,
width: start.width - dy / ratio.current,
left: start.left + dy / ratio.current / 2,
};
}
return {
height: start.height - diff.y,
top: start.top + diff.y,
};
},
middleBottom(diff, start) {
if (keepRatio && ratio.current) {
const dy = diff.y;
return {
height: start.height + dy,
width: start.width + dy / ratio.current,
left: start.left - dy / ratio.current / 2,
};
}
return {
height: start.height + diff.y,
};
},
middleLeft(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
height: start.height - dx * ratio.current,
top: start.top + (dx * ratio.current) / 2,
width: start.width - dx,
left: start.left + dx,
};
}
return {
width: start.width - diff.x,
left: start.left + diff.x,
};
},
middleRight(diff, start) {
if (keepRatio && ratio.current) {
const dx = diff.x;
return {
height: start.height + dx * ratio.current,
top: start.top - (dx * ratio.current) / 2,
width: start.width + dx,
};
}
return {
width: start.width + diff.x,
};
},
};
/**
* 更新状态
*/
const updateBlock = (data: UpdateBlock) => {
if (!moveBlock) return;
const _data = { ...moveBlock, ...data };
if (_data.width < 0) return;
if (_data.height < 0) return;
if (confine) {
if (_data.top < 0) return;
if (_data.left < 0) return;
if (_data.left + _data.width > maxBlock.current.width) return;
if (_data.top + _data.height > maxBlock.current.height) return;
}
console.log('ratio: ', ratio.current);
setMoveBlock(_data);
onUpdate?.(_data);
};
/**
* 拖拽时
*/
const onBlockMouseMove = (e: MouseEvent) => {
if (!canMove || !moveBlock) {
return;
}
const { startX, startY } = prePosition.current;
const diff = {
x: e.clientX - startX,
y: e.clientY - startY,
};
const { top, left } = moveBlock;
updateBlock({
top: top + diff.y,
left: left + diff.x,
});
};
const onBlockMouseDown = (e: React.MouseEvent<any, MouseEvent>) => {
setMoveBlock({
width: e.currentTarget.offsetWidth,
height: e.currentTarget.offsetHeight,
top: e.currentTarget.offsetTop,
left: e.currentTarget.offsetLeft,
});
if (!prePosition.current.dragType) {
prePosition.current = {
dragType: 'block',
startX: e.clientX,
startY: e.clientY,
};
}
};
const onPointMouseMove = (e: MouseEvent, type: MovePoint) => {
if (!prePosition.current.dragType || !moveBlock || !canResize) return;
const { startX, startY } = prePosition.current;
const diff = {
x: e.clientX - startX,
y: e.clientY - startY,
};
const setFn = eventMap[type];
updateBlock(setFn(diff, moveBlock));
};
const onPointMouseDown = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
type: MovePoint,
) => {
// 记录鼠标点击开始位置
prePosition.current = {
dragType: type,
startX: e.clientX,
startY: e.clientY,
};
};
const onMouseUp = () => {
prePosition.current.dragType = undefined;
};
const onMouseMove = (e: MouseEvent) => {
if (!prePosition.current.dragType) return;
if (prePosition.current.dragType === 'block') {
onBlockMouseMove(e);
} else {
onPointMouseMove(e, prePosition.current.dragType);
}
prePosition.current.startX = e.clientX;
prePosition.current.startY = e.clientY;
};
useEffect(() => {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
}, [moveBlock]);
return (
<DragResizeContainer
style={{ backgroundColor: 'transparent' }}
onMouseDown={e => {
maxBlock.current = {
width: e.currentTarget.offsetWidth,
height: e.currentTarget.offsetHeight,
};
}}
>
<div
className={classNames({ container: true, canResize, active, canMove })}
style={moveBlock}
onMouseDown={onBlockMouseDown}
onDrag={e => e.preventDefault()}
>
{children}
{movePoints.map(val => (
<div
key={val}
className={classNames({ point: true, [val]: true })}
onMouseDown={e => onPointMouseDown(e, val)}
></div>
))}
</div>
</DragResizeContainer>
);
};
export default DragResize;