본문 바로가기
Font-end/Javascript

[Drag&Drop] 드래그 앤 드롭으로 Element 움직이기

by 허도치 2020. 3. 2.
0. 서론

 최근에 HTML, CSS, Vanilla Javascript를 이용하여 메모장을 만들었다. 생성된 메모들을 화면에 보여줄 때 단순하게 나열만 해주다니 심심하게 느껴졌다. 그래서, Windows의 Sticky Notes처럼 드래그앤드롭으로 위치를 자유롭게 설정할 수 있도록 기능을 추가해보았는데, 다른데서도 유용하게 사용할 수 있을것 같아서 정리를 해보려고한다.

 그럼 예제를 통해 드래그 앤 드롭 기능을 구현해보도록 하자.

 

 

 

1. 프로젝트 준비
1-1. Chrome Browser

1) IE와 같은 구형브라우저에서는 ES를 적용하려면 BABEL을 이용해야하기 때문에 Chrome Browser를 사용.

2) 다운로드
   - https://www.google.com/intl/ko/chrome/

 

1-2. 프로젝트 구성

1) [ css/common.css ] 파일
   : 전반적인 style을 작성할 css파일.

2) [ js/common.js ] 파일
   : Element에 이벤트를 적용하는 js파일.
   : DOM이 모두 로드된 후 적용할 로직을 이 곳에 작성.

3) [ js/event_handler.js ] 파일
   : Elemet에 적용할 Event Handler 함수들을 작성하는 js파일.

4) [ index.html ] 파일
   : 메인 HTML파일

 

 

 

2. css/common.css
2-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* css/common.js */
 
/* 도화지 크기 설정 */
html, body { padding: 0; margin: 0; width: 100%; height: 100%; }
 
/* 공이 자유자재로 움직일 수 있도록 position을 'absolute'로 설정 */
.ball { position: absolute; }
/* 공을 선택했을 때, 그림자가 생기도록 설정 */
.ball:hover { box-shadow: 0 0 10px 0 darkgray; cursor: pointer; }
 
/* 공의 색상 설정 */
.green { background-color: lightgreen; }
.blue { background-color: lightblue; }
.pink { background-color: lightpink }
 
/* 공의 크기 설정 */
.s200 { width: 200px; height: 200px; border-radius: 200px; }
.s100 { width: 100px; height: 100px; border-radius: 100px; }
.s50 { width: 50px; height: 50px; border-radius: 50px; }
cs

1) [ 7 ln ] - Element의 position을 절대값으로 설정
   : absolute로 설정하면 top, left 등에 의해 위치를 조절할 수 있음.

 

 

 

3. js/event_handler.js
3-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// js/event_handler.js
 
// 공 선택 이벤트 핸들러
function handleMouseDown(event){
  event.preventDefault();;
  
  const balls = document.querySelectorAll(".ball");
  const el = event.target;
  const classList = el.classList;
  
  if!classList.contains("hold") ){
    // 공을 클릭했을 때, 마우스 커서의 XY좌표
    const mouseX = event.clientX;
    const mouseY = event.clientY;
    
    // 선택한 공의 XY좌표 (왼쪽 상단 모서리 기준)
    const ballPos = el.getBoundingClientRect();
    const ballX = ballPos.x;
    const ballY = ballPos.y;
    
    // 선택한 공 안에 있는 마우스 커서의 XY좌표
    const gapX = mouseX - ballX;
    const gapY = mouseY - ballY;
    
    el.setAttribute("gap-x", gapX);
    el.setAttribute("gap-y", gapY);
    
    // 선택한 공을 맨 앞으로 가지고 오기
    const maxPriority = (
      balls.length > 0 
        ? Math.max.apply(nullArray.from(balls).map(ball=>ball.getAttribute("priority"))) 
        : 9999
    ) + 1;
    el.setAttribute("priority", maxPriority);
    el.style["z-index"= maxPriority;
    
    // 선택한 공에 'hold' class를 추가
    classList.add("hold");
  }
}
 
// 공 움직임 이벤트 핸들러
function handleMouseMove(event){
  event.preventDefault();
  
  const el = document.querySelector(".ball.hold");
  if( el ){
    // 움직이는 마우스 커서의 XY좌표
    const mouseX = event.clientX;
    const mouseY = event.clientY;
    
    // 선택한 공 안에 있는 마우스 커서의 XY좌표
    const gapX = el.getAttribute("gap-x");
    const gapY = el.getAttribute("gap-y");
    
    // 마우스 커서의 위치에 따른 공의 XY좌표
    const ballX = mouseX - gapX;
    const ballY = mouseY - gapY;
    
    // 공의 위치를 변경
    el.style.left = ballX+"px";
    el.style.top = ballY+"px";
  }
}
 
// 공 놓기 이벤트 핸들러
function handleMouseUp(event){
  event.preventDefault();
    
  const el = document.querySelector(".ball.hold");
  if( el ){
    // 움직이면 적용된 속성 및 class를 삭제
    el.removeAttribute("gap-x")
    el.removeAttribute("gap-y")
    
    el.classList.remove("hold");
  }
}
cs

1) [ 29-35 ln ] - 선택한 공을 맨 앞으로 가지고 오기
   : 공을 클릭했을 때, z-index의 최대값에 +1을 더해서 가장 앞에 위치하도록 설정.

[ 공의 좌표를 나타낸 그림 ]


2) [ 53-58 ln ] - 마우스가 움직일 때 공의 위치 계산

   : 원래는 마우스의 커서가 ballX, ballY 좌표에 위치하게 됨.
   : 공을 움직일 때, 처음 클릭했던 위치를 고정하려면, 공의 위치와 마우스 위치의 차이값(gapX, gapY)을 구해야함.
   : 마우스가 이동할 때마다 마우스의 위치에서 해당 값을 빼줌으로써 ballX, ballY의 좌표를 구할 수 있음.

 

 

 

4. js/common.js
4-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js/common.js
 
const balls = document.querySelectorAll(".ball");
 
balls.forEach(function(ball, idx){      
  // 공의 우선순위 설정
  let priority = ball.getAttribute("priority");
  if!priority ){
    priority = idx+1;
    ball.setAttribute("priority", priority);
  }
  ball.style["z-index"= priority;
  
  // 공 선택 이벤트 바인딩
  ball.addEventListener('mousedown', handleMouseDown);
});
 
// 마우스 이벤트 바인딩
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
cs

1) [ 7-12 ln ] - 공의 우선순위(인덱스) 설정
   : 우선순위(priority) 속성이 없으면 index 값으로 자동 부여됨.

2) [ 15 ln ] - 공을 선택하는 이벤트를 ball Element에 바인딩

3) [ 19-20 ln ] - 마우스의 이동과 선택 해제 이벤트를 document에 바인딩
   : ball Element에 바인딩 할 경우, 마우스 커서를 빠르게 움직이여서 공을 벗어나면 제어가 안되기 때문.

 

 

 

5. index.html
5-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <link href="css/common.css" rel="stylesheet"/>
    <script src="js/event_handler.js"></script>
  </head>
  <body>
    <div class="ball green s200"></div>
    <div class="ball blue s100"></div>
    <div class="ball pink s50"></div>
    <script src="js/common.js"></script>
  </body>
</html>
cs

1) [ 5~6 ln ] - css와 event_handler.js를 로드

2) [ 9~11 ln ] - 공을 여러개 생성

3) [ 12 ln ] - common.js를 로드
   : <body>의 하단에 javascript를 로드하는 것은 DOM이 다 로드된 후 적용하기 위함.
   : <head>에서 로드할 경우, <div>가 생성되지 않았기 때문에 이벤트가 적용되지 않음.

 

 

 

6. 실행
6-1. index.html을 실행

1) 파일을 더블클릭하여 실행

2) 파일을 Chrome Browser로 드로그앤드롭하여 실행

3) 주소창에 파일의 위치를 입력하여 실행
   : file:///C:/work/javascript/ball/index.html

 

6-2. 실행 결과

1) 공을 선택하면 선택된 공에 그림자가 생기며, 맨 앞으로 이동됨.

2) 공을 움직여보면 사진과 같이 자유자재로 움직일 수 있음.

 

 

 

마치며

- 드로그앤드롭 기능은 구현해봐야겠다고 생각만하고 미루고 있었는데, 직접 만들어보니 생각보다 간단했다. 다만 Browser의 호환성은 따지지 않고 만들었기 때문에 Chrome에서만 잘 돌아갈 것이다.
- 다음에는 이 기능을 이용해서 재밌는 예제를 만들어보고 포스팅해보도록 하겠다.

- 2020/03/03 - [Font-end/Javascript] - [그림판] HTML, CSS, JS로 그림판 만들기

댓글