에디터 개발 프로젝트를 진행하면서, 드래그 및 컴포넌트 선택 기능을 구현해보았습니다.
프로젝트 주소
https://github.com/boostcampwm-2022/web32-bmNotion
onMouseDown
마우스 버튼이 눌려질 때 작동.
onMouseMove
마우스 포인터가 컴포넌트 위에서 움직일때 작동.
onMouseUp
눌렸던 마우스 버튼이 올라올 때 작동.
Wrapper
전체 컴포넌트들을 감싸는 컴포넌트
MainContainerBody
드래그를 시작할 수 있는 컴포넌트
DragRange
드래그 컴포넌트
BlockContentBox
블록
마우스 클릭 위치를 저장한다.
// MainContainerBody component
onMouseDown={(e) => {
setMouseStartPosition({
...mouseStartPosition,
positionX: e.pageX,
positionY: e.pageY,
});
}}
마우스가 클릭 된 상태라면 현재 마우스 커서의 위치를 저장한다.
// Wrapper component
onMouseMove={(e) => {
if (mouseStartPosition.positionX && mouseStartPosition.positionY) {
setMousePosition({ ...mousePosition, positionX: e.pageX, positionY: e.pageY });
}
}}
마우스 버튼이 올라오면서 마우스 위치와 관련된 모든 state를 초기화한다.
// Wrapper component
onMouseUp={(e) => {
setMouseStartPosition({ ...mouseStartPosition, positionX: null, positionY: null });
setMousePosition({ ...mousePosition, positionX: null, positionY: null });
}}
props로 구현했을 때 마우스의 위치가 바뀔 때마다 컴포넌트가 재생성되는 문제가 발생하여
in-line style로 수정하였습니다.
🤕 props로 구현
<DragRange
startPositionX={mouseStartPosition.positionX}
startPositionY={mouseStartPosition.positionY}
positionX={mousePosition.positionX}
positionY={mousePosition.positionY}
/>
interface DragRangeProps {
startPositionX: number | null;
startPositionY: number | null;
positionX: number | null;
positionY: number | null;
}
const DragRange = styled.div<DragRangeProps>`
background-color: rgba(35, 131, 226, 0.15);
width: 100%;
height: 100%;
position: absolute;
left: ${(props) => {
if (props.startPositionX && props.positionX) {
return Math.min(props.startPositionX, props.positionX).toString() + 'px';
}
return null;
}};
top: ${(props) => {
if (props.startPositionY && props.positionY) {
return Math.min(props.startPositionY, props.positionY).toString() + 'px';
}
return null;
}};
width: ${(props) => {
if (props.positionX && props.startPositionX) {
return (
Math.abs(
props.positionX - props.startPositionX
)?.toString() + 'px'
);
}
return 0;
}};
height: ${(props) => {
if (props.positionY && props.startPositionY) {
return (
Math.abs(
props.positionY - props.startPositionY
)?.toString() + 'px'
);
}
return 0;
}};
`;
💊 마우스의 위치가 바뀔 때마다 컴포넌트가 재생성되는 문제 발생
(class name이 바뀐다 = 컴포넌트가 재생성된다)
😝 in-line style로 구현
<DragRange
style={{
left:
mouseStartPosition.positionX && mousePosition.positionX
? Math.min(mouseStartPosition.positionX, mousePosition.positionX).toString() + 'px'
: '0px',
top:
mouseStartPosition.positionY && mousePosition.positionY
? Math.min(mouseStartPosition.positionY, mousePosition.positionY).toString() + 'px'
: '0px',
width:
mouseStartPosition.positionX && mousePosition.positionX
? Math.abs(
mouseStartPosition.positionX - mousePosition.positionX
).toString() + 'px'
: '0px',
height:
mouseStartPosition.positionY && mousePosition.positionY
? Math.abs(
mouseStartPosition.positionY - mousePosition.positionY
).toString() + 'px'
: '0px',
}}
/>
const DragRange = styled.div`
background-color: rgba(35, 131, 226, 0.15);
position: absolute;
`;
선택된 블록들에 selected 클래스를 추가하고,
추가 스타일을 적용해준다.
const BlockContentBox = styled.div<DraggableProps>`
.
.
.
&.selected {
background-color: rgba(35, 131, 226, 0.15);
}
`;
블록의 선택과 해제가 실시간으로 이루어져야 하기 때문에 onMouseMove
이벤트에 추가해준다.
블록이 드래그 컴포넌트의 영역 안일 경우 selected class를 추가해주고, 영역 밖일 경우 selected class를 제거해준다.
// Wrapper component
onMouseMove={(e) => {
if (mouseStartPosition.positionX && mouseStartPosition.positionY) {
if (
mouseStartPosition.positionX &&
mouseStartPosition.positionY &&
mousePosition.positionX &&
mousePosition.positionY
) {
const left = Math.min(mouseStartPosition.positionX, mousePosition.positionX);
const top = Math.min(mouseStartPosition.positionY, mousePosition.positionY);
const width = Math.abs(
mouseStartPosition.positionX - mousePosition.positionX
);
const height = Math.abs(
mouseStartPosition.positionY - mousePosition.positionY
);
const right = left + width;
const bottom = top + height;
blocks.forEach((block, i) => {
const boxTop = // 블록 컴포넌트의 위
const boxBottom = // 블록 컴포넌트의 아래
const boxLeft = // 블록 컴포넌트의 왼쪽
const boxRight = // 블록 컴포넌트의 오른쪽
if (top <= boxBottom && boxTop <= bottom && left <= boxRight && boxLeft <= right) {
block.classList.add('selected');
} else {
block.classList.remove('selected');
}
});
}
}
}}
마우스 클릭시 블록 선택을 초기화해준다.
onMouseDown={(e) => {
blocks.forEach((e) => e.classList.remove('selected'));
}}