CSS&JS/⚡Thinkers

[vanilla JS] 드래그 가능한(움직이는) 모달창 만들기

arancia_ 2021. 11. 1. 16:38

움직이는 모달창 구현까진 완료하였는데 드래그 하던 도중 마우스가 브라우저 밖으로 나가면 이벤트가 씹히거나 윈도우가 드래그(selection)된 상태여서 UX가 굉장히 불편한 현상이 발생한다.

그래서.. 이것저것 찾아보다가 window의 selection을 해제해주면 되겠다 싶었음. 그리고 전보다 훨씬 수월하게 동작한다.

modal - getSelection removeAllRanges.zip
0.00MB

소스코드 다운로드

 

index.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
<!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>smooth modal</title>
<link rel="stylesheet" type="text/css" href="./src/style.css"/>
<script src="./src/main.js" type="module"></script>
</head>
<body>
    
<button id="open_all_modal">OPEN ALL MODAL</button>
 
<div data-include_modal="md_1"></div>
<div data-include_modal="md_2"></div>
<div data-include_modal="md_3"></div>
 
<h1>SMOOTH MODAL</h1>
<h6>only vanilla JS</h6>
<section>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia delectus nobis corrupti voluptas! Corporis ratione nisi, velit nihil ad tempora. Veniam atque id alias iusto accusamus, error harum dicta iure numquam eaque animi possimus perferendis officiis et praesentium quo nisi totam vitae, tempore, quasi quisquam recusandae odio asperiores. Nesciunt eos sequi nisi quaerat maiores in ipsam cumque unde tempora consequatur earum aliquam adipisci ipsa odit quam totam quae sed soluta voluptas, tenetur nihil! Omnis ipsum in molestiae atque pariatur labore sed minus officiis suscipit, tempore facilis perferendis magni commodi cupiditate tenetur nobis ea voluptatem amet, ab quos possimus quisquam cumque!
 
</section>
 
<h2>Clearing Selection is the KEY</h2>
<h3>window.getSelection().removeAllRanges();</h3>
</body>
</html>
cs

 

src/style.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
@charset "utf-8";
:root{
    --gap:30px;
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
    position:relative;overflow:hidden;
    width:100%;min-height:100vh;
    background:#ccc;
}
 
#open_all_modal{
    all:unset;
    display:block;
    width:100%;
    margin:0 auto; padding:1em;
    background:#666; 
    text-align:center;color:#fff;
    cursor:pointer;
}
 
[data-indlcue_modal]{
    position:fixed;
    top:0;left:0;
}
 
    [data-visible="off"]{display:none;}
/*  */
.modal{
    /* outline:3px solid pink; */
    position:fixed; z-index:100;
    top:0;left:0;
    transform:translateX(calc(50vw - 50%)) translateY(calc(50vh - 50%));
    min-width:300px;
}
 
.modal__head{
    /* outline:1px solid red; */
    position:relative; z-index:110;
    width:calc(100% + var(--gap) * 4);
    margin-left:calc(var(--gap) * -2);
    padding:var(--gap) 0;
    cursor:move;
}
.modal__head p{
    position:relative;
    width:calc(100% - var(--gap) * 4);
    margin:0 auto;
    padding:5px 1em;
    background:#000;
    color:#fff;
    pointer-events:none;user-select:none;
}
 
/*  */
.modal__body{
    position:relative;
    margin-top:calc(var(--gap) * -1);
    padding:2rem 1.5rem;
    width:100%;
    background:#fff;
    border:1px solid black;
    border-width:0 1px;
}
 
/*  */
.modal__foot{
    display:flex;flex-flow:row nowrap;
    justify-content:flex-end; align-items:center;
    position:relative;
    width:100%;
    background:#eee;
    border:1px solid black;
    border-width:0 1px 1px 1px;}
 
.modal__foot button{
    all:unset;
    display:block;
    padding:.5rem 1rem;
    font-size:.875rem; font-weight:bold;color:#888;
    cursor:pointer;
}
cs

 

modal/md_1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="modal" style="width:600px;">
    <div class="modal__head">
        <p>HELLO WORLD</p>
    </div><!-- modal__head -->
 
    <div class="modal__body">
        <p>Lorem, ipsum dolor.</p>
    </div><!-- modal__body -->
 
    <div class="modal__foot">
        <button class="btn_close_modal"><span>CLOSE</span></button>
    </div><!-- modal__foot -->
</div>
cs

 

src/main.js

1
2
3
4
5
6
7
8
9
import { include_all_modal } from "./include.js";
 
include_all_modal();
 
const btn_open_modal = document.getElementById('open_all_modal');
btn_open_modal.addEventListener('click',()=>{
    const all_modal = document.querySelectorAll('[data-include_modal]');
    all_modal.forEach(modal=>{modal.dataset.visible = "on"});
});
cs

 

src/include.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { make_drag, set_close_modal } from "./move_modal.js";
 
export async function include_all_modal(){
    const include_modal = document.querySelectorAll('[data-include_modal]');
    for(let dom of include_modal){
        const data = await fetch_html(dom);
        dom.innerHTML = data;
        //움직이게
        make_drag(dom);
        //닫을 수 있게
        set_close_modal(dom);
    }
}//include_all_modal
 
function fetch_html(dom){
    const modal_id = dom.dataset.include_modal;
    const url = `./modal/${modal_id}.html`;
    return fetch(url).then(res => res.text());
}//fetch_html
cs

 

src/move_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
/* ⚡ 모달 Draggble 관련*/
let is_drag_able = false;
let size_win = return_window_size();
let nowX,nowY;
const GAP = -30;
 
/* 🎈🎈🎈🎈 window size */
function return_window_size(){
    return {
        wid : window.innerWidth,
        hei : window.innerHeight
    };
}//return_window_size
 
export function update_window_size(){ size_win = return_window_size(); }
 
/* 🎈🎈🎈🎈 DRAG */
export function make_drag(dom){
    const this_modal = dom.getElementsByClassName('modal')[0];
    const dragHead = this_modal.getElementsByClassName('modal__head')[0];
 
    dragHead.addEventListener('mousedown',(e)=>{start_drag(e,this_modal)});
    dragHead.addEventListener('mousemove',(e)=>{on_drag(e,this_modal)});
    this_modal.addEventListener('mouseup',()=>{stop_drag(this_modal)});
    this_modal.addEventListener('mouseleave',()=>{stop_drag(this_modal)});
}//make_drag
 
/* 클릭시 - 드래그 시작할 준비를 한다. */
function start_drag(e,modal){
    is_drag_able = true;
    modal.dataset.x = e.clientX - modal.getBoundingClientRect().left;
    modal.dataset.y = e.clientY - modal.getBoundingClientRect().top; 
}//start_drag
 
/* 마우스가 떠나거나 클릭 종료시 - 드래그 종료 */
export function stop_drag(modal){
    clear_selection();
    is_drag_able = false;
    if(modal){
        modal.dataset.x = modal.getBoundingClientRect().left;
        modal.dataset.y = modal.getBoundingClientRect().top; 
    }
}//stop_drag
 
/* 클릭한 채로 마우스 이동시 - 드래그 진행 */
function on_drag(e,modal){
    if(!is_drag_able){return;}
    const size_modal = {
        wid : modal.getBoundingClientRect().width,
        hei : modal.getBoundingClientRect().height
    };
 
    const max_movement = {
        left : size_win.wid - size_modal.wid,
        top : size_win.hei - size_modal.hei
    };
    
    const posX = e.clientX - modal.dataset.x;
    const posY = e.clientY - modal.dataset.y;
 
    if(posX <= 0){
        nowX = 0;
    }else if(posX > max_movement.left){
        nowX = max_movement.left;
    }else{
        nowX = posX;}
 
    if(posY <= GAP){
        nowY = GAP;
    }else if(posY > max_movement.top){
        nowY = max_movement.top;
    }else{
        nowY = posY;}
 
    modal.style.transform = `translate(${nowX}px, ${nowY}px)`;
}//on_drag
 
/* CLEAR SELECTION */
export function clear_selection(){
    window.getSelection().removeAllRanges();
    //https://w3c.github.io/selection-api
}//clear_selection
 
/* CLOSE MODAL */
export function set_close_modal(dom){
    const btn_close_modal = dom.getElementsByClassName('btn_close_modal')[0];
    btn_close_modal.addEventListener('click',()=>{
        dom.dataset.visible="off";
    });
}//set_close_modal
cs