# mouse-drag-and-drop

모던 HTML 표준에서는 드래그 앤 드롭 내용이 있습니다.

  • dragstart dragend 이벤트
    • 운영체제의 파일 관리 애플리케이션으로 부터 파일을 드래그하여 브라우저 화면에 드롭하는 기능을 제공함.
    • 한계
      • 특정 영역에서 드래그 하는 것을 막을 수 없음
      • 수평 / 수직으로만 드래그 할 수 없다.
      • 모바일 환경 지원 부족
    • 한계를 극복하기 위해 마우스 이벤트를 이용하여 drag&drop 기능을 구현해봅니다.

DragZone, Droppable, Draggable 및 기타 클래스 등 아키텍처를 구축하는 프레임워크가 있습니다. 대부분은 앞서 드래그와 드롭에 대한 설명과 유사한 작업을 하므로 이해하기 쉽습니다. 때로는 제3의 솔루션 적용보다 쉽게 수행할 수 있습니다.

# drag & drop 알고리즘

  1. mousedown
    • 움직임이 필요한 요소를 준비함.
  2. mousemove
    • position: absoluteleft, top 을 변경
  3. mouseup
    • 드래그 앤 드롭 완료와 관련된 모든 작업 수행

# 공을 드래그 하는 구현

<html>
  <img id="ball" src="https://avatars.githubusercontent.com/u/31977543?s=88&u=abc3f0443f4c82f349f3103cd9010f272db55fe6&v=4"/>
</html>
<script>
  ball.onmousedown = (event) => {
    ball.style.position = 'absolute';
    ball.style.zIndex = 1000;

    // 현재 위치한 부모에서 body 로 직접 이동하여 body 기준으로 위치를 지정함
    document.body.append(ball);

    // 공을 pageX, pageY 좌표 중앙에 위치하게 합니다.
    function moveAt(pageX, pageY) {
      ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
      ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
    }

    // 포인터 아래로 공을 이동시킨다.
    moveAt(event.pageX, event.pageY);

    function onMouseMove(event) {
      moveAt(event.pageX, event.pageY);
    }

    document.addEventListener('mousemove', onMouseMove);
    ball.onmouseup = () => {
      document.removeEventListener('mousemove', onMouseMove);
      ball.onmouseup = null;
    }

    ball.ondragstart = function() {
      return false;
    };
  }
</script>
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

브라우저 자체적으로 이미지나 요소에 대한 드래그 앤 드롭을 지원한다면.

  • drag 를 할 때, 복사된 공을 드래그 하는 동작이 나타날 수 있다.
  • 이럴 때 다음 코드를 추가해 준다.
ball.ondragstart = function() {
  return false;
};
1
2
3

이미지 내의 어떤 좌표를 클릭하면 이미지가 움직여버리는 문제점이 있습니다.

function moveAt(pageX, pageY) {
  ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
  ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
}
1
2
3
4
  • MouseEvent.clientX

    • 이벤트가 발생한 애플리케이션 viewport 내에 수평 좌표를 제공한다
  • Element.getBoundingClientRect()

    • DOMRect 요소의 크기 및 뷰포트를 기준으로 한 위치에 대한 정보를 제공 하는 객체를 반환합니다. image
  • DOMRect

    • 직사각형의 크기와 위치를 나타냅니다.
ball.onmousedown = (event) => {
    // pointer 에서 공의 왼쪽 위 모서리까지의 거리를 저장합니다.
    // 공을 드래그 하는 동안 이 거리를 유지한다.
    const shiftX = event.clientX - ball.getBoundingClientRect().left;
    const shiftY = event.clientY - ball.getBoundingClientRect().top;
    
    ball.style.position = 'absolute';
    ball.style.zIndex = 1000;
    document.body.append(ball);
    
    // 초기 이동을 고려한 좌표 (pageX, pageY)에서
    // 공을 이동합니다.
    function moveAt(pageX, pageY) {
      ball.style.left = pageX - shiftX + 'px';
      ball.style.top = pageY - shiftY + 'px';
    }
    
    // 포인터 아래로 공을 이동시킨다.
    moveAt(event.pageX, event.pageY);
    
    function onMouseMove(event) {
      moveAt(event.pageX, event.pageY);
    }
    
    document.addEventListener('mousemove', onMouseMove);
    ball.onmouseup = () => {
      document.removeEventListener('mousemove', onMouseMove);
      ball.onmouseup = null;
    }
}

ball.ondragstart = function() {
  return false;
}
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

# 잠재적 드롭 대상

# 마우스 이벤트의 맨 위 요소에서만 이벤트가 발생하는 문제

실행예시

blue 에서는 이벤트 핸들러가 동작하지 않습니다. red 가 만약 다른 곳으로 이동하게 된다면 blue 가 디스플레이에 보이게 되고, blue 의 이벤트 핸들러가 동작하는 것을 기대해야 합니다.

<style>
  div {
    width: 50px;
    height: 50px;
    position: absolute;
    top: 0;
  }
</style>
<div style="background:blue" onmouseover="alert('never works')"></div>
<div style="background:red" onmouseover="alert('over red!')"></div>
1
2
3
4
5
6
7
8
9
10

document.elementFromPoint

  • 윈도우 기준 좌표에서 가장 많이 중첩된 요소를 반환한다.

다음과 같이 구현 합니다.

<html>
  <img src="https://en.js.cx/clipart/soccer-gate.svg" id="gate" class="droppable">
  <img src="https://en.js.cx/clipart/ball.svg" id="ball">
</html>
<script>

// 잠재적 드롭 가능한 요소
let currentDroppable = null;

ball.onmousedown = function (event) {

  let shiftX = event.clientX - ball.getBoundingClientRect().left;
  let shiftY = event.clientY - ball.getBoundingClientRect().top;
  ball.style.position = 'absolute';
  ball.style.zIndex = 1000;
  document.body.append(ball);

  moveAt(event.pageX, event.pageY);

  function moveAt(pageX, pageY) {
    ball.style.left = pageX - shiftX + 'px';
    ball.style.top = pageY - shiftY + 'px';
  }
  function onMouseMove(event) {
    moveAt(event.pageX, event.pageY);

      ball.hidden = true;
      // elemBelow : 드롭할 수 있는 공의 아래 요소, 축구골대
      const elemBelow = document.elementFromPoint(event.clientX, event.clientY);
      ball.hidden = false;
    
      // 공을 윈도우 밖으로 드래그 했을 때,
      // clientX, clientY 가 윈도우 밖에 있으면, elementFromPoint 는 null 반환
      if (!elemBelow) return;
      
      // 잠재적으로 드롭할 수 있는 요소를 'droppable' 클래스로 지정한다.  축구 골대
      const droppableBelow = elemBelow.closest('.droppable');

      // 둘다 null 일 수 있다. 들어오거나, 나가거나에 해당할 때,
      // currentDroppale = null : 축구 골대 밖에 있을 때,
      // droppableBelow = null : 이벤트 동안 축구 골대 안에 없을 때,  
      if (currentDroppable !== droppableBelow) {
        if (currentDroppable) {
          // 축구 골대 밖에 있을 때 강조 제거
          leaveDroppable(currentDroppable);
        }
        currentDroppable = droppableBelow;
        if (currentDroppable) {
          // 축구 골대 안으로 들어오는 것을 처리하는 로직
          enterDroppable(currentDroppable);
        }
      }
      document.addEventListener('mousemove', onMouseMove);
      ball.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        ball.onmouseup = null;
      };
  }
  function enterDroppable(elem) {
    elem.style.background = 'pink';
  }
  
  function leaveDroppable(elem) {
    elem.style.background = '';
  }

  ball.ondragstart = function() {
    return false;
  };
}

</script>
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

# 예시 2

<html>
<body>
  <div id="slider" class="slider">
    <div class="thumb"></div>
  </div>

  <script>
    const thumb = slider.querySelector('.thumb');

    thumb.onmousedown = function(event) {
      event.preventDefault(); // prevent selection start (browser action)

      let shiftX = event.clientX - thumb.getBoundingClientRect().left;
      // shiftY not needed, the thumb moves only horizontally

      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);

      function onMouseMove(event) {
        let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left;

        // the pointer is out of slider => lock the thumb within the bounaries
        if (newLeft < 0) {
          newLeft = 0;
        }
        let rightEdge = slider.offsetWidth - thumb.offsetWidth;
        if (newLeft > rightEdge) {
          newLeft = rightEdge;
        }

        thumb.style.left = newLeft + 'px';
      }

      function onMouseUp() {
        document.removeEventListener('mouseup', onMouseUp);
        document.removeEventListener('mousemove', onMouseMove);
      }

    };

    thumb.ondragstart = function() {
      return false;
    };

  </script>

</body>
</html>
<style>
.slider {
  border-radius: 5px;
  background: #E0E0E0;
  background: linear-gradient(left top, #E0E0E0, #EEEEEE);
  width: 310px;
  height: 15px;
  margin: 5px;
}

.thumb {
  width: 10px;
  height: 25px;
  border-radius: 3px;
  position: relative;
  left: 10px;
  top: -5px;
  background: blue;
  cursor: pointer;
}
</style>
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

# Reference

https://ko.javascript.info/mouse-drag-and-drop#ref-246