CSS&JS/⚡Thinkers

[JS]vanilla JS - class 문법으로 모달창 구현하기 (draggable)

arancia_ 2022. 4. 6. 11:16

진짜 최종의_최종의_최종.psd 같지만 하여간...

깃허브 : https://github.com/OhIkmyeong/move_modal
 

GitHub - OhIkmyeong/move_modal: 모달창 움직이기 sample

모달창 움직이기 sample . Contribute to OhIkmyeong/move_modal development by creating an account on GitHub.

github.com

여기서 modal 폴더에 들어가시면 소스 있습니다.

얻어갈 수 있는것 :

  • fetch()를 이용한 비동기로 html을 include하기
  • class 문법으로 drag 가능한 modal 만들기
  • mousedown시 mousemove 이벤트 달기 
  • mouseup시 mousemove 이벤트 지워주기
  • 모달을 드래그 하는 도중, 모달 내의 텍스트까지 긁히는 현상 방지하기 (user-select:none/auto)

 

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
<!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>Move Modals...</title>
    <link rel="stylesheet" type="text/css" href="./css/reset.css"/>
    <link rel="stylesheet" type="text/css" href="./css/modal.css"/>
    <script src="./js/main.js" type="module"></script>
</head>
<body>
    <header>
        <h1>Move Modals...!</h1>
        <h3>Remove MouseMove Event When MouseUp Occurs!!!</h3>
        <div>
            <button class="btn-open-md" data-modal-open="modal_1">open modal 001</button>
            <button class="btn-open-md" data-modal-open="modal_2">open modal 002</button>
            <button class="btn-open-md" data-modal-open="modal_3">open modal 003</button>
        </div>
    </header>
    <main>
        <div class="box">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aperiam expedita amet saepe? Sit explicabo expedita alias? Quas numquam ducimus saepe.</div>
        <div class="box">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aperiam expedita amet saepe? Sit explicabo expedita alias? Quas numquam ducimus saepe.</div>
        <div class="box">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aperiam expedita amet saepe? Sit explicabo expedita alias? Quas numquam ducimus saepe.</div>
    </main>
 
    <!-- [MODALS] -->
    <div class="wrap-modal" data-include="./include/modal_1.html" data-modal="modal_1"></div>
    <div class="wrap-modal off" data-include="./include/modal_2.html" data-modal="modal_2"></div>
    <div class="wrap-modal off" data-include="./include/modal_3.html" data-modal="modal_3"></div>
</body>
</html>
cs

 

modal.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
@charset "utf-8";
.wrap-modal{
    position:fixed;
    top:0;left:0;
    
    transition:opacity .3s;}
 
    .wrap-modal.off{
        opacity:0;
        pointer-events:none;user-select:none;}
 
.md-bg{
    position:fixed;
    top:0;left:0;
    width:100%;height:100vh;
    background:rgba(0,0,0,.1);
    backdrop-filter:blur(10px);
}
.modal{
    position:fixed;
    top:0;left:0;
    min-width:300px; max-width:50vw;
    background:#fff;
    border:2px solid black;
    box-shadow:
    -5px -5px 5px rgba(255,255,255,.5),
    5px 5px 10px rgba(0,0,0,.2);}
 
.md-head{
    display:flex; flex-flow:row nowrap;
    justify-content:space-between; align-items:center;
    position:relative;
    padding:.5em .5em .5em 1em;
    background:#000;
    font-size:14px; color:#fff;
    user-select:none;
    cursor:grab;}
 
    .btn-close-md{
        display:block; position:relative;
        width:24px; aspect-ratio:1/1;
        background:rgba(255,255,255,.2);
        border:none;}
 
        .btn-close-md::before,
        .btn-close-md::after{
            content:'';display:block;position:absolute;
            top:50%;left:50%;
            width:2px; height:15px;
            background:red;
            pointer-events:none;user-select:none;}
        .btn-close-md::before{
            transform:translate(-50%,-50%) rotate(45deg);}
        .btn-close-md::after{
            transform:translate(-50%,-50%) rotate(-45deg);}
 
.md-body{padding:1rem;}
 
/* [위치] */
.wrap-modal[data-modal="modal_1"] .modal{transform:translate(30vw,10vh);}
.wrap-modal[data-modal="modal_2"] .modal{transform:translate(10vw,20vh);}
.wrap-modal[data-modal="modal_3"] .modal{transform:translate(40vw,50vh);}
cs

 

include/modal_sample.html

1
2
3
4
5
6
7
8
9
10
<div class="md-bg"></div>
<div class="modal">
    <section class="md-head">
        <p class="md-title">Modal 003</p>
        <button class="btn-close-md" title="close modal" data-modal-close="modal_3"></button>
    </section>
    <section class="md-body">
        <h3>BBO-WAP!</h3>
    </section>
</div><!-- modal -->
cs

 

main.js

1
2
3
import { Common } from "./func/common.js";
 
Common();
cs

 

func/common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Include } from "./include.js";
import { Modal } from "./modal.js";
 
export async function Common(){
    const INCLUDE = new Include();
    const MODAL = new Modal();
    
    await INCLUDE.init();
    MODAL.init();
 
    window.addEventListener('click',e=>{
        const target = e.target;
        // OPEN
        if(target.dataset.modalOpen){MODAL.open(target);}
        // CLOSE
        if(target.dataset.modalClose){MODAL.close(target);}
    });
}//Common
cs

 

func/include.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class Include{
    async init(){
        const $$dom = document.querySelectorAll('[data-include]');
        for(let $dom of $$dom){
            const data = await this.fetch_html($dom);
            $dom.innerHTML = data;
            $dom.removeAttribute('data-include');
        }//for
    }//init
 
    fetch_html($dom){
        const url = $dom.dataset.include;
        return fetch(url).then(res=>res.text());
    }//fetch_html
}//class-Include
cs

 

func/modal.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
export class Modal{
    constructor(){}//constructor
 
    init(){
        const $$wrap = document.querySelectorAll('[data-modal]');
        $$wrap.forEach($wrap => { new Draggable($wrap);});
    }
 
    open(target){
        const mdName = target.dataset.modalOpen;
        const $wrap = document.querySelector(`[data-modal="${mdName}"]`);
        const $bg = $wrap.getElementsByClassName('md-bg')?.[0]; 
        if($bg){
            document.body.style.overflowY = 'hidden';
        }//if
        $wrap.classList.remove('off');
    }//open
 
    close(target){
        const mdName = target.dataset.modalClose;
        const $wrap = document.querySelector(`[data-modal="${mdName}"]`);
        document.body.style.overflowY = 'auto';
        $wrap.classList.add('off');
    }//close
}//class-Modal
 
/* ----------- 모달 드래그 ------------ */
/* 윈도우 사이즈 */
const WINDOW_SIZE = {
    wid : window.innerWidth,
    hei : window.innerHeight
};
 
/* 윈도우 사이즈 업데이트 */
function update_window_size(){
    WINDOW_SIZE.wid = window.innerWidth;
    WINDOW_SIZE.hei = window.innerHeight;
}//update_window_size
 
/* 윈도우 드래그 해제 */
function clear_selection(){
    if (window.getSelection().empty) {  
        // Chrome
        window.getSelection().empty();
    }else if(window.getSelection().removeAllRanges){  
        // Firefox
        window.getSelection().removeAllRanges();
    }
    //https://w3c.github.io/selection-api
    //https://stackoverflow.com/questions/3169786/clear-text-selection-with-javascript
}//clear_selection
 
class Draggable{
    constructor($wrap){
        this.$modal = $wrap.getElementsByClassName('modal')[0];
        this.$header = this.$modal.getElementsByClassName('md-head')[0];
        this.$body = this.$modal.getElementsByClassName('md-body')[0];
        this.is_draggable = false;
        this.SIZE = {
            wid : this.$modal.getBoundingClientRect().width,
            hei : this.$modal.getBoundingClientRect().height}
        this.LIMIT = {
            right : WINDOW_SIZE.wid - (this.SIZE.wid / 2),
            bottom : WINDOW_SIZE.hei - (this.SIZE.hei / 2)}
        this.POS = {
            last : {x:null, y:null},
            final : {x:null, y:null}}
 
        //실행
        this.init();
    }//constructor
 
    /* 실행 시작 */
    init(){
        if(!this.$header){return;}
        this.$header.addEventListener('mousedown'this.ready_to_drag);
        this.$modal.addEventListener('mouseup',this.stop_drag);
        window.addEventListener('mouseleave',this.stop_drag);
    }//init
 
    /* 드래그 준비 */
    ready_to_drag = (e) =>{
        this.is_draggable = true;
        this.POS.last.x = e.clientX  - this.$modal.getBoundingClientRect().left;
        this.POS.last.y = e.clientY  - this.$modal.getBoundingClientRect().top;
 
        /* update limit size.. */
        update_window_size();
        this.LIMIT.right = WINDOW_SIZE.wid - (this.SIZE.wid / 2);
        this.LIMIT.bottom = WINDOW_SIZE.hei - (this.SIZE.hei / 2);
 
        /* add Event */
        window.addEventListener('mousemove'this.on_drag);
    }//ready_to_drag
    
    /* 드래그 중 */
    on_drag = (e) => {
        if(!this.is_draggable){return;}
        this.$body.style.userSelect = 'none';
        const currX = e.clientX - this.POS.last.x;
        const currY = e.clientY - this.POS.last.y;
 
        /* X축 */
        if(currX <= (this.SIZE.wid / -2)){
            this.POS.final.x = (this.SIZE.wid / -2);
        }else if(currX > this.LIMIT.right){
            this.POS.final.x = this.LIMIT.right;
        }else{
            this.POS.final.x = currX;}
            
        /* Y축 */
        if(currY <= 0){
            this.POS.final.y = 0;
        }else if(currY > this.LIMIT.bottom){
            this.POS.final.y = this.LIMIT.bottom;
        }else{
            this.POS.final.y = currY;}
 
        /* 최종 css 적용 */
        const {final:{x,y}} = this.POS;
        this.$modal.style.transform = `translate(${x}px,${y}px)`; 
    }//on_drag
 
    /* 드래그 끝 */
    stop_drag = () => {
        this.is_draggable = false;
        this.$body.style.userSelect = 'auto';
        // clear_selection();
        this.POS.last.x = this.$modal.getBoundingClientRect().left;
        this.POS.last.y = this.$modal.getBoundingClientRect().top;
 
        //removeEvent
        window.removeEventListener('mousemove'this.on_drag);
    }//stop_drag
}//class-Draggable
cs