CSS&JS/⚡Thinkers

[CSS/JS]vanilla JS로 핸들러 2개인 range input 구현하기(기본)

arancia_ 2023. 10. 11. 09:09

보통 input[type="range"]는 핸들러가 1개 뿐이다.
근데 만일 min, max 핸들러가 2개인 input을 구현하고 싶다면?

유의사항

  • 완전 기본의 기본. 안되는게 더 많아요! 되지 않는 항목들은 다음과 같음
  • 현재 min, max 제한 없이 왔다갔다함. (min이 max보다 더 오른쪽으로 이동한다든가)
  • step에 대한 snap 없음
  • 귀찮아서 window.resize에 대한 초기화 안 해놓음

그대로 갔다 쓰면 문제가 많을거에요 ㅋㅋ 걍 원리만 이해하시고 변형하고 추가 수정해서 쓰면 됩니다.

 

데모 사이트 : https://Ohikmyeong.github.io/range-slider-minmax/v1

 

HTML

html은 별거 없습니다. 그냥 css랑 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
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./css/main.css">
    <script src="./js/main.js" type="module"></script>
</head>
<body>
    <template>
        <section class="slider-wrap">
            <article class="slider-bar">
                <div class="slider-bar-on"></div>
            </article>
    
            <button class="slider-btn slider-btn-min" title="min">
                <span class="slider-btn-label">99999999</span>
            </button>
            <button class="slider-btn slider-btn-max" title="max">
                <span class="slider-btn-label">999</span>
            </button>
    
            <ul class="slider-param">
                <li>0</li>
                <li>100</li>
                <li>200</li>
                <li>300</li>
                <li>400</li>
                <li>500</li>
                <li>600</li>
            </ul>
 
            <div class="slider-amount">asdf</div>
        </section><!-- slider-wrap -->
    </template>
</body>
</html>
cs

slider.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
92
93
94
95
96
97
98
99
100
101
102
103
@charset "utf-8";
/* wrap */
.slider-wrap{
    position:relative;
    width:90%;max-width:800px;
}
 
/* param */
.slider-param{
    display:flex;
    justify-content:space-between; align-items:center;
    position:relative;
    width:100%;
    margin-top:3px;
    text-align:center; font-size:12px;
}
 
.slider-param li{
    position:relative;
    min-height:20px;
    border-left:1px solid #ccc;
    /* padding-left:20px; */
    width:calc(100% / var(--item-count));
}
.slider-param li:last-child{border-right:1px solid #ccc;}
 
.slider-param li::before{
    content:attr(data-label-left); display:block; position:absolute;
    top:calc(100% + 5px);left:0;
    transform:translateX(-50%);
}
.slider-param li:last-child::after{
    content:attr(data-label-right); display:block; position:absolute;
    top:calc(100% + 5px);right:0;
    transform:translateX(50%);
}
 
/* bar */
.slider-bar{
    position:relative; overflow:hidden;
    width:100%; height:20px;
    background:#ccc;
    border-radius:20px;
}
 
.slider-bar-on{
    position:absolute;
    top:0;left:0;
    width:100%; height:100%;
    background:royalblue;
    clip-path:polygon(var(--bar-min) 0, var(--bar-max) 0, var(--bar-max) 100%, var(--bar-min) 100%);
}
 
/* button */
.slider-btn{
    display:block;position:absolute;
    left:0; top:0;
    width:30px; aspect-ratio:1/1;
    cursor:pointer;
    background:transparent;
    /* background:red; */
    border:none;
}
.slider-btn::before{
    content:'';display:block;position:absolute; box-sizing:border-box;
    left:-15px;top:-5px;
    width:100%; height:100%;
    background:royalblue;
    border:4px solid #fff;
    border-radius:50%;
    box-shadow:0 0 0 2px rgba(0,0,0,.2);
    cursor:pointer;
}
 
/* button-label */
.slider-btn-label{
    display:block;position:absolute;
    left:50%;bottom:100%;
    transform:translate(calc(-50% - 15px), -70%);
    padding:7px 14px;
    background:black;
    border-radius:2px;
    font-family:inherit; font-size:16px; font-weight:bold; color:#fff;
    user-select:none; pointer-events:none;
}
 
.slider-btn-label::after{
    content:'';display:block;position:absolute;
    left:50%;top:100%;
    transform:translateX(-50%);
    border:8px solid transparent; border-top-color:black;
}
 
/* slider-amount */
.slider-amount{
    position:relative;
    margin-top:50px;
    text-align:center;font-size:20px;font-weight:bold;
}
.slider-amount::before{
    content:'Amount : ';
    font-weight:normal;
}
cs

main.js

1
2
3
4
5
6
7
import { SliderBuilder } from "./SliderBuilder.js";
 
new SliderBuilder()
.set_min(-300)
.set_max(300)
.set_step(100)
.init();
cs

 

SliderBuilder.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
import { Slider } from "./Slider.js";
 
export class SliderBuilder{
    constructor(){
        this.info = {
            min : 0,
            max : 100,
            step : 10,
        };
    }
    set_min(min = 0){
        this.info.min = min ?? 0;
        return this;
    }
 
    set_max(max = 100){
        this.info.max = max ?? 100;
        return this;
    }
 
    set_step(step = 10){
        this.info.step = step ?? 10;
        return this;
    }
 
    init(){
        return new Slider(this.info).init();
    }
}//SliderBuilder
cs

Slider.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
import { SliderDomMaker } from "./SliderDomMaker.js";
 
export class Slider extends SliderDomMaker{
    constructor(info){
        super();
        this.info = info;
        this.param = [];
        this.$btnCurr = null;
        this.btnPos = {
            down : null,
            now : null
        }
    }//constructor
 
    init(){
        /* DOM 그리기 */
        this.draw_slider();
        
        /* 버튼 */
        this.move_btn_max_center();
        
        /* 라벨 */
        this.change_label_min(this.param[0]);
        this.change_label_max(this.param[parseInt(this.param.length / 2)]);
 
        /* 이벤트 더하기 */
        this.$btnMin.addEventListener('mousedown',this.on_mouse_down,{once:true})
        this.$btnMax.addEventListener('mousedown',this.on_mouse_down,{once:true})
 
    }//init
 
    /* ----------- */
    change_label(INFO = {}){
        const {value, $lbl} = INFO;
        $lbl.textContent = value;
    }//change_label
    
    change_label_min(value){
        this.change_label({value : value, $lbl : this.$lblMin});
    }//change_label_min
    
    change_label_max(value){
        this.change_label({value : value, $lbl : this.$lblMax});
    }//change_label_max
    
    /* ----------- */
    on_mouse_down = (e) =>{
        this.$btnCurr = e.currentTarget;
        this.btnPos.down = e.clientX;
        window.addEventListener('mousemove'this.on_mouse_move);
        window.addEventListener('mouseup'this.on_mouse_up, {once:true});
    }//on_mouse_down
 
    on_mouse_move = (e) => {
        const xDown = e.clientX - this.btnPos.down;
        const {left} = this.$btnCurr.getBoundingClientRect();
        const {left:wrapLeft, width:wrapWidth} = this.$bar.getBoundingClientRect();
        const leftPrev = left - wrapLeft; 
        const x = leftPrev + xDown;
        const limitRight = wrapWidth;
        let finalX = x;
        if(x < 0){finalX = 0;}
        if(x > limitRight){finalX = limitRight;}
        this.btnPos.down = e.clientX;
 
        /* 01. 버튼 옮기기 */
        this.$btnCurr.style.transform = `translateX(${finalX}px)`;
 
        /* 02. bar 조정 */
        const perX = finalX / wrapWidth * 100;
        const isMin = this.is_btn_curr_min();
        this.$barOn.style.setProperty(`--bar-${isMin ? "min" : "max"}`, `calc(${perX}%)`);
 
        /* 03. label 텍스트 변경 */
        const {min,max} = this.info;
        const value = parseInt(min + ((max - min) * perX / 100));
        if(isMin){
            this.change_label_min(value);
        }else{
            this.change_label_max(value);
        }
 
        /* 04.amount 텍스트 변경 */
        this.display_amount();
    }//on_mouse_move
 
    on_mouse_up = (e) =>{
        window.removeEventListener('mousemove',this.on_mouse_move);
        this.$btnCurr.addEventListener('mousedown',this.on_mouse_down,{once:true})
        this.$btnCurr = null;
    }//on_mouse_up
 
    /* ----------- */
    is_btn_curr_min(){
        if(this.$btnCurr.classList.contains(this.clss.btnMin)) return true;
        return false;
    }//is_btn_curr_min
 
    /* ----------- */
    display_amount(){
        const max = Number(this.$lblMax.textContent);
        const min = Number(this.$lblMin.textContent);
        const amount = max - min;
        this.$amount.textContent = amount;
    }//display_amount
}//Slider
cs

SliderDomMaker.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
export class SliderDomMaker{
    constructor(){
        this.clss = {
            wrap : "slider-wrap",
            bar : "slider-bar",
            barOn : "slider-bar-on",
            btn : "slider-btn",
            btnMin : "slider-btn-min",
            btnMax : "slider-btn-max",
            btnLabel : "slider-btn-label",
            param : "slider-param",
            amount : "slider-amount"
        }
        
        this.$wrap = null;
        this.$bar = null;
        this.$barOn = null;
        this.$btnMin = null;
        this.$btnMax = null;
        this.$lblMin = null;
        this.$lblMax = null;
        this.$param = null;
        this.$amount = null;
 
        this.btnWidth = 30;
    }//constructor
 
    draw_slider(){
        /* wrap */
        this.$wrap = this.make_wrap();
        
        /* bar */
        this.$bar = this.make_bar();
        this.$wrap.appendChild(this.$bar);
 
        /* button */
        const [$btnMin, $btnMax] = this.make_btns();
        this.$wrap.appendChild($btnMin);
        this.$wrap.appendChild($btnMax);
 
        /* param */
        this.$param = this.make_param();
        this.$wrap.appendChild(this.$param);
        
        /* amount */
        this.$amount = this.make_amount();
        this.$wrap.appendChild(this.$amount);
 
        /* 최종 */
        document.body.appendChild(this.$wrap);
    }//draw_slider
 
    make_wrap(){
        const $wrap = document.createElement('SECTION');
        $wrap.classList.add(this.clss.wrap);
        return $wrap;
    }//make_wrap
 
    make_bar(){
        const $bar = document.createElement('ARTICLE');
        const $barOn = document.createElement('DIV');
        $bar.classList.add(this.clss.bar);
        $barOn.classList.add(this.clss.barOn);
 
        $barOn.style.setProperty('--bar-min',"0%");
        $barOn.style.setProperty('--bar-max',"50%");
 
        $bar.appendChild($barOn)
        this.$barOn = $barOn;
 
        return $bar;
    }//make_bar
 
    make_btns(){
        const $btnMin = this.make_btn(true);
        const $btnMax = this.make_btn(false);
        return [$btnMin, $btnMax];
    }//make_btns
 
    make_btn(isMin = true){
        const $btn = document.createElement('BUTTON');
        $btn.classList.add(this.clss.btn);
        $btn.classList.add(isMin ? this.clss.btnMin : this.clss.btnMax);
 
        const $lbl = document.createElement('SPAN');
        $lbl.classList.add(this.clss.btnLabel);
        $lbl.textContent = 0;
        $btn.appendChild($lbl);
 
        if(isMin){
            this.$btnMin = $btn;
            this.$lblMin = $lbl;
        }else{
            this.$btnMax = $btn;
            this.$lblMax = $lbl;
        }
 
        return $btn;
    }//make_btn
 
    make_param(){
        const $param = document.createElement('UL');
        $param.classList.add(this.clss.param);
 
        const {min,max,step} = this.info;
        for(let i=min; i<=max; i+=step){
            if(i<max){
                const $li = document.createElement('LI');
                // $li.textContent = i;
                $param.appendChild($li);
                $li.dataset.labelLeft = i;
                if(i+step >= max){$li.dataset.labelRight = i + step;}
            }
            this.param.push(i);
        }
 
        $param.style.setProperty('--item-count',this.param.length - 1);
 
        return $param
    }//make_param
 
    make_amount(){
        const $amount = document.createElement('DIV');
        $amount.classList.add(this.clss.amount);
        $amount.textContent = "0";
        return $amount;
    }
 
    /* -------------- */
    move_btn_max_center(){
        const {width} = this.$wrap.getBoundingClientRect();
        const x = (width / 2- (this.btnWidth / 2);
        this.$btnMax.style.transform = `translateX(${x}px)`;
        console.log(x);
    }//move_btn_max_center
}//SliderDomMaker
cs