CSS&JS/⚡Thinkers

vanilla JS로 sortable list 구현하기(3.2)

arancia_ 2021. 12. 9. 17:33

(3)에서는 진정한 depth 개념의 아코디언으로 구현은 하였지만,
depth1의 sub이 아이템이 0개이면 다음번에 추가가 안되는 맹점이 있어서
강제로 JS에서 만일 옮기고 난 뒤 아이템이 0개라면 그 드래그를 취소하도록 짰다.

이번에는 아이템이 0개라도 다시 추가 가능하도록 했다.
간단한 CSS 트릭...

++

드래그시 발생하는 고스트 이미지도 지우고 커스텀으로 바꿨다.

3만 5천번 강조해도 모자란 Codepen@Tino Thamjarat님의 원본 코드

뭔가 계속 최종_진짜_최종__ 같지만 뭐..

 

html

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
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test_3.2</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script src="./main.js" type="module"></script>
</head>
<body>
이렇게 하면.. 리스트가 비었을때 ㅎ.. 더이상 안 들어가...
<ul id="myUL">
    <li>
        <div class="d1">A.나는 드래그 하면 안되양~~~</div>
        <ul class="sub">
            <li class="d2-item">
                <input type="checkbox" />
                <span>키킬 001</span>
            </li>
            <li class="d2-item">
                <input type="checkbox" />
                <span>키킬 002</span>
            </li>
            <li class="d2-item">
                <input type="checkbox" />
                <span>키킬 003</span>
            </li>
        </ul>
    </li>
    <li>
        <div class="d1">B.나는 드래그 하면 안되양~~~</div>
        <ul class="sub">
            <li class="d2-item">가가가</li>
            <li class="d2-item">나나나나나나나</li>
            <li class="d2-item">다ㅏㅏㅏㅏㅏㅏㅏ</li>
        </ul>
    </li>
    <li>
        <div class="d1">A.나는 드래그 하면 안되양~~~</div>
        <ul class="sub">
            <li class="d2-item">001</li>
            <li class="d2-item">002</li>
            <li class="d2-item">003</li>
        </ul>
    </li>
</ul>
 
</body>
</html>
cs

CSS

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
@charset "utf-8";
*{margin:0;padding:0;box-sizing:border-box;}
html,body{width:100%;min-height:100vh; background:#666;}
li{list-style-type:none;}
 
#myUL{
    position:relative;
    margin:3rem auto; padding:10px;
    width:500px;
    border:1px solid black;}
 
#myUL li{
    position:relative;
    width:100%;}
 
.d1{
    padding:1.5em 1.5em;
    background:rgb(247, 169, 169);
    border:1px solid black;
    user-select:none;}
 
.sub{
   position:relative;
   /* min-height:57px;  */
}
.d2-item{
    position:relative;
    margin:10px 0; padding:.8em 1.5em;
    background:#eee;
    cursor:pointer; user-select:none;}
 
    .d2-item.dontDrag{
        width:100%;height:10px;
        margin:0;padding:0;
        background:black;
        opacity:0;
    }
 
.ghost{background:rgb(93, 93, 182);color:#fff;}
 
.fake{
    position:fixed;
    top:0;left:0;
    width:478px;
    padding:.8em 1.5em;
    background:#eee;
    opacity:.8;
    transition:opacity .2s;
    pointer-events:none;user-select:none;
}
cs

fake.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function make_fake(dragEl){
    const fake = document.createElement('DIV');
    fake.classList.add('fake');
    fake.style.opacity = 0;
    fake.innerHTML = dragEl.innerHTML;
    document.body.appendChild(fake);
    setTimeout(()=>{fake.style.opacity = 0.8;},100);
}
 
export function move_fake(evt){
    const {clientX, clientY} = evt;
    const fake = document.getElementsByClassName('fake')[0];
 
    const domX = evt.target.getBoundingClientRect().left;
 
    const finalX = clientX - (fake.offsetWidth / 2);
    const finalY = clientY - (fake.offsetHeight / 2);
    fake.style.transform = `translate(${finalX}px, ${finalY}px)`;
}
 
export function delete_fake(){
    const fake = document.getElementsByClassName('fake')[0];
    document.body.removeChild(fake);
}
cs

getPostion.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// let dragIndex, dragSource
export function getMouseOffset(evt) {
    const targetRect = evt.target.getBoundingClientRect()
    const offset = {
        x: evt.pageX - targetRect.left,
        y: evt.pageY - targetRect.top
    }
    return offset
}//getMouseOffset
 
export function getElementVerticalCenter(el) {
    const rect = el.getBoundingClientRect()
    return (rect.bottom - rect.top) / 2
}//getElementVerticalCenter
cs

sortable.js

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
79
80
81
82
83
84
85
86
87
88
89
import { delete_fake, make_fake, move_fake } from './fake.js';
import {getMouseOffset,getElementVerticalCenter} from './getPosition.js'
 
/* 원본 코드는 https://codepen.io/vtno/pen/MXmpoy */
 
const fakeGhost = new Image();
fakeGhost.src = './transparent.png';
 
export function sortable(rootEl) {
    let dragEl;
    // Add ghost item 왜냐면 하나만 남았을때 더 추가 안 되는 상황 막으려고
    const all_sub = rootEl.querySelectorAll('.sub');
    all_sub.forEach(sub =>{
        const li = document.createElement('LI');
        li.classList.add('d2-item');
        li.classList.add('dontDrag');
        sub.appendChild(li);
    });
    // Making all siblings movable
    const all_list_item = rootEl.getElementsByClassName('d2-item'); 
    [].slice.call(all_list_item).forEach(item =>{
        if(!item.classList.contains('dontDrag')){
            item.draggable = true;
        }
    });
 
    // Sorting starts
    rootEl.addEventListener('dragstart'function (evt) {
        dragEl = evt.target; // Remembering an element that will be moved
 
        //fake 만들기
        make_fake(dragEl);
 
        //고스트 이미지 지우기
        evt.dataTransfer.setDragImage(fakeGhost,0,0);
 
        // Subscribing to the events at dnd
        rootEl.addEventListener('dragover', _onDragOver);
        rootEl.addEventListener('dragend', _onDragEnd);
 
        setTimeout(()=>{
            // If this action is performed without setTimeout, then
            // the moved object will be of this class.
            dragEl.classList.add('ghost');
        }, 0)
    }); //dragstart
 
    // Function responsible for sorting
    function _onDragOver(evt) {
        evt.preventDefault();
        evt.dataTransfer.dropEffect = 'move';
 
        const target = evt.target;
 
        // `내가 드래그한건 ${dragEl.innerText}
        // 드래그 하며 지나치는 타겟은 ${target.innerText}`);
 
        if (target && target !== dragEl && target.classList.contains('d2-item')) {
            // Sorting
            const offset = getMouseOffset(evt)
            const middleY = getElementVerticalCenter(evt.target)
            const targetIDX = [].indexOf.call(all_list_item,target);
            if (offset.y > middleY) {
                const temp_parent = target.parentElement;
                targetIDX !== -1 && temp_parent.insertBefore(dragEl, target.nextSibling);
            } else {
                const temp_parent = target.parentElement;
                targetIDX !== -1 && temp_parent.insertBefore(dragEl, target);
            }
 
        }//if
 
        //fake 움직이기
        move_fake(evt);
    }//_onDragOver
 
    // End of sorting
    function _onDragEnd(evt) {
        evt.preventDefault();
 
        //이벤트 및 클래스 제거
        dragEl.classList.remove('ghost');
        rootEl.removeEventListener('dragover', _onDragOver);
        rootEl.removeEventListener('dragend', _onDragEnd);
 
        //fake 제거
        delete_fake();
    }//_onDragEnd
}//sortable
cs

 

소스코드 파일

sortable__3.2.zip
0.00MB