CSS&JS/⚡Thinkers

[vanilla JS] 터치 슬라이더, 그랩 슬라이더

arancia_ 2021. 8. 2. 16:24

지난번에 만든 터치슬라이더는 웹에서는 터치이벤트를 제공하지 않기 때문에 반쪽짜리 슬라이더였음.
이번에는 웹에서는 mousedown + mousemove + mouseup / 터치 지원 환경에서는 touchmove, touchend시 좌우로 SNAP 되는 바닐라 자바스크립트 슬라이더를 만들어봤다.

 

 

 

다음은 소스코드

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
<!DOCTYPE html>
<html lang="en">
<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>touch, grab</title>
<link href="style.css" rel="stylesheet" type="text/css"/>
<script src="main.js" type="module"></script>
</head>
<body>
    
<button id="btn_pause">일시정지</button>
 
<section id="sect">
    <ul id="slide">
        <li><div class="content">0</div></li>
        <li><div class="content">1</div></li> 
        <li><div class="content">2</div></li> 
        <li><div class="content">3</div></li> 
        <li><div class="content">4</div></li>  
        <li><div class="content">5</div></li>
    </ul><!-- slide -->
    
    <button data-arrow="prev"></button>
    <button data-arrow="next"></button>
</section><!-- sect -->
 
<ul id="pager"></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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@charset "utf-8";
:root{
    --wrap:50vh;
}
 
*{margin:0;padding:0;box-sizing:border-box;} li{list-style-type:none;}
button,input{font-family:inherit;font-size:inherit;color:inherit; cursor:pointer;}
 
html,body{
    display:flex;flex-wrap:wrap;flex-direction:column;
    justify-content:center;
    align-items:center;
    overflow:hidden;
    width:100%;min-height:100vh;
    background:#ccc;
    font-family:sans-serif; font-size:24px;}
 
 
#btn_pause{padding:.5em 2em;}
 
/*  */
#sect{
    position:relative; overflow:hidden;
    width:var(--wrap); aspect-ratio:1/1;
    margin:1rem auto;
    border:2px solid #000;}
 
/*  */
#slide{
    display:flex; flex-wrap:nowrap;
    position:relative;
    border:1px solid #000;
    transition:transform .3s;
    cursor:grab;}
 
#slide li{
    flex:none;
    display:flex;justify-content:center;align-items:center;
    position:relative;
    background:#fff;
    border:1px solid red;
    width:var(--wrap); aspect-ratio:1/1;}
 
#slide li .content{pointer-events:none; user-select:none;}
 
/*  */
[data-arrow]{
    position:absolute;
    width:3rem; aspect-ratio:1/1;
    top:calc(50% - 1.5rem);
    border:none;
    border-radius:50%;}
 
[data-arrow = "prev"]{left:0;}
[data-arrow = "next"]{right:0;}
 
/*  */
#pager{
    display:flex; justify-content:center;
    position:relative;
    width:var(--wrap);;}
 
#pager li{
    position:relative;
    height:15px; aspect-ratio:1/1;
    margin:20px 5px;
    background:black;
    border-radius:10rem;
    cursor:pointer;}
 
#pager li.on{aspect-ratio:3/1; background:red;}
cs

 

JS - main.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
import {make_pager, get_index_pager} from "./pager.js"
import {arrow_slide } from "./arrow.js";
import {stop_slide, autoSlide, adjust_slider} from "./slide.js";
import { touch_moveSlider } from "./touch.js";
import { drag_mouseDown } from "./drag.js";
 
 
export const pause = document.getElementById('btn_pause');
export const sect = document.getElementById('sect');
export const slide= document.getElementById('slide');
export const pager = document.getElementById('pager');
 
export let WID = window.innerHeight / 2;
export const LEN = slide.children.length;
export const MAX = WID*(LEN-1)*-1;
export let BOUND_LEFT = sect.getBoundingClientRect().left;
 
export let btn_txt = {
    play : "다시 재생?",
    pause : "일시정지"
}
 
export let move = {
    now : 0
    ,PER : WID / (window.innerHeight / 50)
    ,ORDER : 0
    ,e_prev : 0
    ,e_now : 0
}
 
export let pager_num = {eq : 0};
 
 
/* ⚠ ✨ 실행 -  페이저 생성 */
make_pager(slide,pager);
 
/* 윈도우 리사이즈 관련 */
window.addEventListener('resize',()=>{
    WID = window.innerHeight / 2;
    move.PER = WID / (window.innerHeight / 50);
});
 
/* ✨ 실행 - 자동슬라이드 */
setTimeout(autoSlide, 1000);
 
/* ✨ 실행 - 터치 */
sect.addEventListener('touchmove',touch_moveSlider);
sect.addEventListener('touchend',adjust_slider);
 
/* ✨ 실행 - 마우스다운, 마우스무브, 마우스업 */
sect.onmousedown  = drag_mouseDown;
 
 
/* ✨ 실행 - 버튼 - 일시정지 */
pause.addEventListener('click',()=>{
    switch(pause.innerText){
        case btn_txt.pause :
            stop_slide();
            break;
                
        case btn_txt.play :
            autoSlide();
            pause.innerText = btn_txt.pause;
        break;
 
        default :
            stop_slide();
            break;
    }
}); //click
 
/* ✨ 실행 - 좌우 버튼 클릭시 */
sect.addEventListener('click',(e)=>{
    const target = e.target;
    if(!target.dataset.arrow){return;}
    arrow_slide(e);
    return;
}); //click
 
 
/* ✨ 실행 - pager 클릭시 */
pager.addEventListener('click',(e)=>{    
    const thisLI = e.target;
    if(thisLI.tagName !== "LI"){return;}
 
    stop_slide();
 
    get_index_pager(thisLI);
}); //click
 
cs

 

JS - pager.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
import {WID, LEN, pager_num, pager, move} from "./main.js"
import { move_slide } from "./slide.js";
 
/* 최초 - 슬라이드 개수에 맞춰 페이저 자동 생성 */
export function make_pager(slide,pager){
    const slide_li = slide.children;
    for(let i=0; i<slide_li.length; i++){
        const pager_li = document.createElement('LI');
        pager.appendChild(pager_li);
    }
    pager.children[0].classList.add('on');
}//make_pager
 
/* 현재 페이저 계산 및 갱신 */
export function cacul_pager(num){
    reset_pager();
    pager_num.eq += num;
    if(pager_num.eq < 0){pager_num.eq = LEN - 1;}
    if(pager_num.eq > LEN - 1){pager_num.eq = 0;}
 
    display_pager(pager_num.eq);
}//cacul_pager
 
/* INDEX 가져오기 */
export function get_index_pager(thisLI){
    const thisEQ = Array.prototype.indexOf.call(pager.children,thisLI);
    reset_pager();
    display_pager(thisEQ);
 
    move.now = WID * thisEQ * -1;
    move_slide();
}//get_index_pager
 
/* DISPLAY */
export function display_pager(order){
    pager.children[order].classList.add('on');
}//display_pager
 
/* RESET */
export function reset_pager(){
    for(let dot of pager.children){dot.classList.remove('on');}
}//reset_pager
cs

 

JS - arrow.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
import {WID} from "./main.js"
import {cacul_move_slide, stop_slide} from "./slide.js";
import { reset_pager,cacul_pager } from "./pager.js";
 
/* 02. 좌우 버튼 */
export function arrow_slide(e){
    stop_slide();
    reset_pager();
    const direct = e.target.dataset.arrow; 
    switch(direct){
        case "prev" :
            cacul_pager(-1);
            cacul_move_slide(WID);
            break;
            
            case "next" :
            cacul_pager(1);
            cacul_move_slide(-WID);
            break;
 
        default : 
            break;
    }
    
}
cs

 

JS - slide.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
import {slide, MAX, WID, pause, btn_txt, move, pager_num} from "./main.js"
import {reset_pager, cacul_pager, display_pager} from "./pager.js"
 
 
/* (공통) 슬라이드 움직임 */
export function cacul_move_slide(wid){
    move.now += wid;
    if(move.now > 0){
        move.now = MAX;
    }else if(move.now < MAX){
        move.now = 0;
    }
    move_slide();
}//cacul_move_slide
 
export function move_slide(){
    slide.style.transform = `translateX(${move.now}px)`;
}//move_slide
 
/* (공통) 슬라이드 멈춤 */
export function stop_slide(){ 
    pause.innerText = btn_txt.play;  
    clearTimeout(set_move); 
}//stop_slide
 
/* (공통) 좌표값 계산*/
export function is_prev_next(){
    if(move.e_now > move.e_prev){
        move.now += move.PER;
    }else{
        move.now -= move.PER;
    }
    move_slide();
    move.e_prev = move.e_now;
}
 
/* (공통) 종료 후 슬라이드 위치 조정 */
export function adjust_slider(){
    if(move.now > 0){
        move.now = 0;
    }else if(move.now < MAX){
        move.now = MAX;
    }
 
    move.ORDER = Math.round(move.now / WID);
    move.now = move.ORDER * WID;
 
    move_slide();
 
    reset_pager();
    pager_num.eq = move.ORDER * -1 
    display_pager(pager_num.eq);
}//adjust_slider
 
/* (공통) 자동 슬라이드 */
let set_move;
 
export function autoSlide(){
    cacul_pager(1);
    cacul_move_slide(-WID);    
 
    set_move = setTimeout(autoSlide,2000);
}
 
cs

 

JS - touch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { BOUND_LEFT, move } from "./main.js";
import { display_pager } from "./pager.js";
import { is_prev_next, stop_slide } from "./slide.js";
 
 
export function touch_moveSlider(e){
    e = e || window.event;
    e.preventDefault;
 
    if(e.target.dataset.arrow){return;}
 
    stop_slide();
    move.e_now = e.changedTouches[0].clientX - BOUND_LEFT;
   
    is_prev_next();
}//touch_moveSlider
cs

 

JS - drag.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
import {sect,slide,BOUND_LEFT, move} from "./main.js"
import { adjust_slider, is_prev_next, stop_slide } from "./slide.js";
 
export function drag_mouseDown(e){
    e = e || window.event;
    e.preventDefault;
    stop_slide();
    
    slide.style.cursor = 'grabbing';
    
    document.onmouseup = drag_close;
    sect.onmousemove = drag_slide;
}
 
function drag_slide(e){
    e = e || window.event;
    e.preventDefault;
 
    if(e.target.dataset.arrow){return;}
 
    move.e_now = e.clientX - BOUND_LEFT;
 
    is_prev_next();
}//drag_slide
 
function drag_close(){
    slide.style.cursor  = 'grab';
    adjust_slider();
    document.onmouseup = null;
    sect.onmousemove = null;
}//drag_close
cs