CSS&JS/⚡Thinkers

커스텀 셀렉트 박스 만들기(2) - HTML DOM에는 select만 작성하고 vaniila JS로 알아서 DIV 생성

arancia_ 2022. 2. 8. 11:13

1탄이었던 이전 포스팅 주소 : https://aosceno.tistory.com/492

 

커스텀 셀렉트 박스 만들기 - HTML DOM에는 select만 작성하고 vaniila JS로 알아서 DIV 생성

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 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..

aosceno.tistory.com

감사한 덧글을 보고 class문법으로 리팩토링 해볼겸 기능도 추가하였습니다~!

 

추가된 기능

  • 키보드 동작 (위 아래 화살표, tab, Enter, Space)에 반응
  • option값에 따라 기존 select dom을 삭제할 수도 있고 삭제 안 할수도 있습니다용
  •  
아 이걸 어떻게 쓰란거임?

만일 급한데 걍 갖다 붙여서 써야하는 상황이라면 이렇게 하세요

  1.  html : 커스텀 셀렉트로 변환해야할 select에 class="csSel"을 달아주세요
  2.  css : 1번의 html파일에서 해당 스타일을 추가로 스타일 링크들 중 최하단에 추가로 링크를 걸어주시거나, 기존 스타일시트의 최하단에 붙여넣기 해주세요.
  3. js
    1. module 파일로 컨트롤 하고 있는 경우 - 해당 module js파일에서 csSel을 import 해서 써주세요. (참고 main.js)
    2. head에 defer로 스크립트를 여러개 걸거나 body 최하단에 스크립트들을 불러오는 경우 : csSel.js의 스크립트를 src로 걸어주시고, 그 위치보단 아래에서 new csSel(option)을 해주세요. (option은 아예 안 써도 무방합니다.) 

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
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
<!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>custom_select v2.0</title>
<link rel="stylesheet" type="text/css" href="./style.css"/>
<script src="./main.js" type="module"></script>
</head>
<body>
    <header>
        <h1>custom_select v2.0</h1>
        <div>
            하지만 가장 좋은것은 그냥 시멘틱 태그인 select를 쓰는 것이지요....
            <br>커스텀 셀렉트같은걸 쓴다면 웹표준을 지키기 위해 추가해야할 기능이 더욱 많아지기 때문입니다. (e.키보드로도 동작 되게끔)
        </div>
        <div>추가한 기능</div>
        <ol>
            <li>그냥 csSel이라는 클래스만 select dom에 달아주면 알아서!</li>
            <li>
                <dl>
                    <dt>option을 줄 수 있어요 (개발자모드로 확인용, 원래 select가 보이게 할 것인가 아닌가)</dt>
                    <dd>오리지날 select dom의 값을 사용할거라면 기본형으로 쓰시고</dd>
                    <dd>custom select의 data-value 값을 사용할거라면, make_custom 메쏘드에서 insertBefore구문 다음에 select를 삭제해주세요.</dd>
                </dl>
            </li>
            <li>키보드로 focus도 되고. 화살표로는 위아래로. Enter나 space시 선택되게 할 거에용</li>
            <li>추가되는 custom_select는 block레벨임으로 container의 display을 잘 조정해보세요~</li>
        </ol>
    </header>
 
    <main>
        <div class="your_container">
            <div>가장 기대되는 게임은 무엇인가요?</div>
            <select class="csSel">
                <option>:: 선택 ::</option>
                <option value="overwatch_2">오버워치2</option>
                <option value="princessMaker_9">프린세스메이커9</option>
                <option value="bubblebubble3000">보글보글3000</option>
            </select><!-- csSel -->
 
            <div>이런 식으로 select 뒤에 어떤 요소가 오든.. 상관이 없어요</div>
        </div>
 
        <div class="your_container">
            <div>사실 5개 이하의 설문지면 radio로 대체하도록 합시다. 근데 예제 만들기 귀찮아앙</div>
            <select class="csSel">
                <option>:: 선택 ::</option>
                <option value="t1">t1</option>
                <option value="t2">t2</option>
                <option value="t3">t3 만일 내가 절라게 길다면 어떻게 반응할까잉</option>
                <option value="t4">t4</option>
                <option value="t5">t5</option>
            </select><!-- csSel -->
        </div>
    </main>
 
 
    <footer>
        <div>깃허브 : <a href="https://github.com/OhIkmyeong" target="_blank">github.com/OhIkmyeong</a></div>
        <div>티스토리 : <a href="https://aosceno.tistory.com" target="_blank">aosceno.tistory.com</a></div>
    </footer>
 
 
    <template>
        <div class="csSel_wrap">
            <div class="csSel_selected" tabindex="0" data-value="">선택된건 여기엥</div>
            <ul class="csSel_list">
                <li tabindex="0" data-value="">test01</li>
            </ul>
        </div>
    </template>
</body>
</html>
cs



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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@charset "utf-8";
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
    width:100%;min-height:100vh;
    background:#e1e1e3;}
 
header{
    position:relative;
    width:70%;
    margin:1rem auto; padding:2rem 3rem;
    background:#fafafc;}
 
header ol{padding-left:3rem; margin-top:.5rem;}
dl{margin-bottom:1em;}
dt{font-weight:bold;}
dd{padding-left:1em; margin-top:.5em;}
 
/*  */
main{
    position:relative;
    width:70%;
    margin:2rem auto; padding:3rem;
    border:1px solid #d2d2d2;}
 
/*  */
.your_container{
    display:flex; flex-flow:row wrap;
    justify-content:center; align-items:center;
    position:relative;
    width:100%;
    margin:2rem auto; padding:1em;
    border:1px solid black;}
 
/* csSel */
.csSel{
    /* 개발 확인용으로 보는것입니다 ^*^ */
    /* 기존 select를 살리고 가리기만 하고 싶으시다면 css에서 수정하세요. */
    /* 아니라면 js에서 삭제해버리세요. */
    position:relative;
    opacity:.5;
    margin-left:1em;
    pointer-events:none; user-select:none;
}
    
/* csCel_wrap */
.csSel_wrap{
    display:flex;flex-flow:column nowrap;
    position:relative;
    margin-left:1em;}
 
    /* 선택된건 이렇게 보여줄거에요 */
    .csSel_selected{
        position:relative;
        padding:1em; padding-right:4em;
        background:#fff; border:2px solid dodgerblue;
        user-select:none;cursor:pointer;
        transition:all .3s linear;}
 
    .csSel_selected:focus{outline:5px solid dodgerblue;}
 
    .csSel_selected::after{
        content:'';display:block;position:absolute;
        top:45%;right:1em;
        width:0; height:0;
        border:.4em solid transparent;
        border-top-color:dodgerblue;}
 
    /* 선택 가능한 리스트는 이렇게 보입니다. */
    .csSel_list{
        position:absolute;
        top:100%;left:0;
        width:100%;
        background:#fff;
        border:1px solid #000;
        opacity:0; pointer-events:none; transform:translateY(-50%);}
 
        /* selected가 focus되어야만 보입니다 */
        .your_container:focus-within .csSel_list{
            z-index:10;
            opacity:1; pointer-events:all; transform:translateY(0);
            transition:all .3s;}
 
    .csSel_list li{
        list-style-type:none;
        position:relative;
        width:100%;
        padding:1em;
        border-top:1px solid #d2d2d2;
        user-select:none; cursor:pointer;}
 
        .csSel_list li:first-child{border-top:none;}
 
        .csSel_list li:hover{background:#f5f5f7;}
        .csSel_list li:focus{
            outline:5px solid dodgerblue;
            background:navy; color:#fff;}
 
cs



main.js

1
2
3
4
5
6
7
8
9
10
11
12
import { csSel } from "./csSel.js";
 
//option값은 줘도 되고 안 줘도 됩니다. 기본값은 모두 false에요.
//(== 기존 select를 삭제하고, 보이지 않게 합니다.);
//현재 데모에선 select를 삭제하지 않고, 보이게 하고 있습니다.
 
const option = {
    delete : false//기존 select를 제거하지 않습니다.
    visible: true //기존 select를 가리지 않고 보이게 합니다. ※visible이 true여도, delete가 true면 visible은 false가 됩니다.
};
 
new csSel(option);
cs



csSel.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
export class csSel{
    constructor(option){
        this.$csSel = undefined;
        this.option = option ?? {};
 
        this.set_option();
        this.init();
    }//constructor
    ////////// METHOD //////////
    /* 옵션 정리 */
    set_option(){
        this.option.delete = this.option.delete ?? false;
        this.option.visible = this.option.visible ?? false;
        if(this.option.delete === true){
            this.option.visible = false;}
    }//set_option
 
    /* init */
    init(){
        this.get_all_dom();
        this.$csSel.forEach($select => {this.make_custom($select);});
    }//init
 
    /* get all selects */
    get_all_dom(){
        const all_dom = document.getElementsByClassName('csSel');
        this.$csSel = Array.from(all_dom);
    }//get_all_dom
 
    /* make custom select */
    make_custom($select){
        $select.tabIndex = -1;
        const options = Array.from($select);
        const $parent = $select.parentElement;
        const $wrap = this.make_wrap();
        
        //selected 추가
        const $selected = this.make_selected();
        $wrap.appendChild($selected);
 
        //list 추가
        const $list = this.make_list(options);
        $wrap.appendChild($list);
 
        //전체 추가
        $parent.insertBefore($wrap,$select.nextSibling);
 
        //사이즈 조정 및 selected 추가
        this.set_size($wrap);
 
        /* 개발자모드 관련 : select를 보이겠음? */
        if(!this.option.visible){this.not_visible($select);}
 
        /* 기존 select를 죽일것인가? */
        if(this.option.delete){
            this.delete_select($parent,$select);
        }
 
 
        //이벤트 달아주기
        this.addEvent($wrap);
    }//make_custom
 
    make_wrap(){
        const $wrap = document.createElement('DIV');
        $wrap.classList.add('csSel_wrap');
        return $wrap;
    }//wrap
 
    make_selected(){
        const $selected = document.createElement('DIV');
        $selected.classList.add('csSel_selected');
        $selected.tabIndex = 0;
        $selected.textContent = '';
        $selected.dataset.value = '';
        return $selected;
    }//make_selected
 
    make_list(options){
        const $list = document.createElement('UL');
        $list.classList.add('csSel_list');
 
        options.forEach(option =>{
            const $li = document.createElement('LI');
            $li.tabIndex = 0;
            $li.dataset.value = option.value ?? "";
            $li.textContent = option.textContent;
            $list.appendChild($li);
        });
 
        return $list;
    }//make_list
 
    set_size($wrap){
        const $list = Array.from($wrap.querySelector('.csSel_list').children);
 
        //가장 긴걸 찾고
        const longest = {len:0, li:$list[0]};
        $list.forEach($li => {
            if($li.textContent.length >= longest.len){
                longest.len = $li.textContent.length;
                longest.li = $li;
            }
        });
 
        //임시로 넣어서 바꿨다가
        const $selected = $wrap.querySelector('.csSel_selected');
        const longVal = longest.li.dataset.value;
        const longContent = longest.li.textContent;
        this.change_selected($selected,longVal,longContent);
 
        //사이즈만 고정하고
        const size = $selected.getBoundingClientRect().width;
        $wrap.style.minWidth = `${size}px`;
 
        //다시 0으로 바꿔주셈 ㅎㅎ
        const firstVal = $list[0].dataset.value;
        const firstContent = $list[0].textContent;
        this.change_selected($selected,firstVal,firstContent);
    }//set_size
 
    /* not_visible */
    not_visible($select){
        $select.style.position = "absolute";
        $select.style.opacity = 0;
        $select.style.overflow = "hidden";
        $select.style.width = 0;
        $select.style.height = 0;
    }//not_visible
 
    /* delete_select */
    delete_select($parent,$select){
        $parent.removeChild($select);
    }//delete_select
 
    /* add EventListener */
    addEvent($wrap){
        const $selected = $wrap.querySelector('.csSel_selected');
        const $csSel = $wrap.querySelector('.csSel_list');
        const $all_li = $csSel.children;
 
        /* selected 관련 이벤트 */
        $selected.addEventListener('keydown',(e)=>{
            switch(e.code){
                case "Escape" :
                    e.target.blur();
                    break;
                
                case "ArrowDown":
                case "ArrowUp":
                    this.get_last_selected($selected,$csSel,$all_li);
                    break;
            }//switch
        });
 
        $selected.addEventListener('focus', ()=>{this.get_last_selected($selected,$csSel,$all_li);});
 
        /* list 관련 이벤트 */
        $csSel.addEventListener('click',this.on_select);
        $csSel.addEventListener('keydown',(e)=>{
            const target = e.target;
            switch(e.code){
                case "Escape" :
                    target.blur();
                    break;
 
                case "ArrowDown" :
                    e.preventDefault();
                    const $next = target.nextElementSibling ?? $all_li[0];
                    $next.focus();
                    break;
                case "ArrowUp" :
                    e.preventDefault();
                    const $prev = target.previousElementSibling ?? $all_li[$all_li.length - 1];
                    $prev.focus();
                    break;
                case "Enter" :
                case "Space" :
                    this.on_select(e);
                    break;
            }//switch
        });
    }//addEvent
 
    //on select
    on_select = (e) =>{
        const target = e.target;
        if(target.tagName !== "LI"){return;}
 
        const val = target.dataset.value;
        const content = target.textContent;
        const $selected = target.parentElement.previousElementSibling;
        this.change_selected($selected,val,content);
 
        /* 기존 select dom 바꿔주기 */
        if(!this.option.delete){
            const $realSelect = target.parentElement.parentElement.previousElementSibling;
            const selectThis = $realSelect.querySelector(`[value="${val}"]`) ?? $realSelect.children[0];
            selectThis.selected = true;
        }//if
 
        //일정 시간뒤 포커싱을 해제한다.
        setTimeout(()=>{
            target.blur();
        }, 200);
    }//on_select
 
    /* 가장 마지막으로 선택됬던거 가져오기 */
    get_last_selected($selected,$csSel,$all_li){
        console.log('ㅋㅋ');
        const val = $selected.dataset.value;
        const lastSelected = $csSel.querySelector(`[data-value="${val}"]`) ?? $all_li[0];
        lastSelected.focus();
    }//get_last_selected
 
    /* custom select의 seleted 바꾸기 */
    change_selected($selected,val,content){
        $selected.dataset.value = val;
        $selected.textContent = content;
    }//change_selected
}//class-csSel
cs




깃허브 주소(소스코드를 다운받을 수 있어요~) :
포스팅 이후에도 찔끔찔끔 수정한 최신코드는 깃허브에 있습니다!

https://github.com/OhIkmyeong/csSel_2

 

GitHub - OhIkmyeong/csSel_2: custom select v2.0

custom select v2.0. Contribute to OhIkmyeong/csSel_2 development by creating an account on GitHub.

github.com