CSS&JS/👀Study and Copy

[JS]Trello 스타일의 drag & drop 구현하기

arancia_ 2024. 9. 27. 15:26

챗지피티가 정말 좋아졌다... 불과 2년전까지만 해도 머리털 쥐뜯었는데... 기본으로 짜준 코드를 바탕으로 다듬어봄

 

Demo

See the Pen Trello by Oh Ikmyeong (@dpffpself) on CodePen.



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
<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Trello</title>
    <link rel="stylesheet" href="./style.css">
    <script src="./main.js" type="module"></script>
</head>
 
<body>
    <form id="frm">
        <input type="text" name="iptAdd" placeholder="type then enter"/>
    </form>
    <div class="wrap">
        <div id="temp">추가한 아이템은 여기로 옴</div>
        <ul class="list">
            <li class="item" draggable="true">
                <div class="item-content">
                    Hello World(1)
                </div>
            </li>
        </ul>
        <ul class="list">
            <li class="item" draggable="true">
                <div class="item-content">
                    Hacked(2)
                </div>
            </li>
        </ul>
        <ul class="list">
            <li class="item" draggable="true">
                <div class="item-content">
                    Overwatch(3)
                </div>
            </li>
        </ul>
    </div>
</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
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
90
91
@charset "utf-8";
 
{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
html,
body {
    background: #34333f;
}
 
body {
    display: flex;
    flex-flow: column nowrap;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}
 
[type="text"] {
    display: block;
    padding: 0 10px;
    height: 42px;
    width: 300px;
}
 
.wrap {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 40px;
    position: relative;
    padding: 40px;
    width: 100%;
    max-width: 1400px;
}
 
#temp{
    display:flex;flex-flow:column nowrap;
    justify-content:flex-start; align-items:flex-start;
    gap:10px;
    position:relative;
    padding:40px;
    background:black;
    color:#fff;
}
 
.list {
    display: flex;
    flex-flow: column nowrap;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 10px;
    position: relative; overflow:auto;
    width: 100%; max-height:60vh;
    background: #eee;
    padding: 20px;
    border-radius: 12px;
}
 
.item {
    list-style-type: none;
    position: relative;
    width:100%;
    cursor: pointer;
}
 
.item-content {
    width: 100%;
    padding: 20px;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 4px;
    color:#000;
    pointer-events:none;
    user-select: none;
    transition:opacity .3s;
}
 
.item.dragging{
    background:rgba(65, 105, 225, 0.265);
    border:2px dotted #aaa;
    cursor:move;
}
 
.item.dragging  .item-content{opacity:0;}
 
.item.nextsib{
    border-top:2px solid royalblue;
}
cs

 

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Trello } from "./Trello.js";
import { TrelloView } from "./TrelloView.js";
 
const $form = document.getElementById("frm");
 
const TRELLO = new Trello()
.set_lists(document.querySelectorAll(".list"))
.set_items(document.querySelectorAll(".item"))
.init();
 
$form.addEventListener("submit",(e)=>{
    e.preventDefault();
    const content = String(e.target.iptAdd.value).trim();
    if(!contentreturn;
    e.target.iptAdd.value = "";
    
    const $item = TrelloView.make_item(content);
    const $temp = document.getElementById("temp");
    $temp.appendChild($item);
 
    TRELLO.add_events_to_item($item);
});
cs

 

TrelloView.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class TrelloView{
    static make_item(content){
        const $item = document.createElement("LI");
        $item.classList.add("item");
        $item.draggable = "true";
 
        const $content = document.createElement("DIV");
        $content.classList.add("item-content");
        $content.textContent = content;
        $item.appendChild($content);
 
        return $item;
    }//make_item
}//class-TrelloView
cs

 

Trello.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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
export class Trello {
    constructor() {
        this.$$list = null;
        this.$$item = null;
        this.$dragging = null;
        this.clss = {
            on: "dragging",
            next: "nextsib"
        }
    }//constructor
 
    set_lists($$list) {
        this.$$list = $$list;
        return this;
    }//set_lists
 
    set_items($$item) {
        this.$$item = $$item;
        return this;
    }//set_items
 
    init() {
        this.$$item.forEach(this.add_events_to_item);
 
        this.$$list.forEach(this.add_events_to_list);
        return this;
    }//init
 
    /**
     * 
     * @param {*} $item 
     */
    add_events_to_item = ($item) => {
        this.add_drag_start_item($item);
        this.add_drag_end_item($item);
    }//add_events_to_item
 
    /**
     * 
     * @param {*} $list 
     */
    add_events_to_list = ($list) => {
        this.add_drag_over_list($list);
        this.add_drag_leave_list($list);
        this.add_drag_drop_list($list);
    }//add_events_to_list
 
    /**
     * 아이템에 이벤트 추가
     */
    add_drag_start_item($item) {
        $item.addEventListener("dragstart", () => {
            this.$dragging = $item;
            $item.classList.add(this.clss.on);
        });
    }//add_drag_start_item
 
    /**
     * 아이템에 이벤트 추가
     */
    add_drag_end_item($item) {
        $item.addEventListener("dragend", () => {
            this.$dragging = null;
            $item.classList.remove(this.clss.on);
        });
    }//add_drag_end_item
 
    /**
     * 
     * @param {*} $list 
     */
    add_drag_over_list($list) {
        $list.addEventListener("dragover", (e) => {
            e.preventDefault();
            const { clientY } = e;
 
            this.remove_all_nextsib_style($list);
 
            const $afterElem = this.get_drag_after_item({ $list, clientY });
            if ($afterElem) { this.nextsib_style($afterElem); }
        });
    }//add_drag_over_list
 
    /**
     * 
     * @param {*} $list 
     */
    add_drag_leave_list($list) {
        $list.addEventListener("dragleave", () => {
            this.remove_all_nextsib_style($list);
        });
    }//add_drag_leave_list
 
    /**
     * 
     * @param {*} $list 
     */
    add_drag_drop_list($list) {
        $list.addEventListener("drop", (e) => {
            e.preventDefault();
            const { clientY } = e;
            const $afterElem = this.get_drag_after_item({ $list, clientY });
            this.remove_all_nextsib_style($list);
            if ($afterElem) {
                $list.insertBefore(this.$dragging, $afterElem);
            } else {
                $list.appendChild(this.$dragging);
            }
        });
    }//add_drag_drop_list
 
    /**
     * 
     * @param {*} $list 
     */
    remove_all_nextsib_style($list) {
        const $$item = $list.querySelectorAll(".item");
        $$item.forEach($item => { $item.classList.remove(this.clss.next); })
    }//remove_all_nextsib_style
 
    /**
     * 
     * @param {*} $item 
     */
    nextsib_style($item) {
        $item.classList.add(this.clss.next);
    }//nextsib_style
 
    /**
     * 
     * @param {*} param0 
     */
    get_drag_after_item({ $list, clientY }) {
        const $$item = Array.from($list.querySelectorAll(".item"));
 
        const result = $$item.reduce((prev, $item) => {
            const { top, height } = $item.getBoundingClientRect();
            const offset = clientY - top - (height / 2);
            if (offset < 0 && offset > prev.offset) {
                return { offset, element: $item }
            } else {
                return prev;
            }
        }, { offset: Number.NEGATIVE_INFINITY });
 
        return result?.element;
    }//get_drag_after_item
}//class-Trello
cs