PageSourceSearch

https://chest.or.kr/common/v2026/js/renew.base.js?_v=20260925063835

js chest.or.kr collected 2026-09-24 21:39:17 UTC 9,323 bytes, 279 lines download raw bytes

1/**
2 * renew.base.js — 2026 재사용 UI 위젯
3 * - custom-select 등 마크업 기반 공통 컴포넌트 스크립트를 모은다.
4 * - 레이아웃/Lenis/헤더 런타임은 ui.common.js 유지.
5 */
6
7/* =============================================================================
8 * custom-select — 네이티브 select 유지 + button/list UI
9 * ============================================================================= */
10(function (win, doc) {
11    'use strict';
12
13    var OPEN_CLASS = 'custom-select--open';
14    var PORTAL_ATTR = 'data-custom-select-portaled';
15    var INIT_ATTR = 'data-custom-select-ready';
16
17    function getList(wrap) {
18        var btn = wrap.querySelector('.custom-select__btn');
19        var listId = btn && btn.getAttribute('aria-controls');
20        if (listId) {
21            var byId = doc.getElementById(listId);
22            if (byId) return byId;
23        }
24        return wrap.querySelector('.custom-select__list');
25    }
26
27    function getPortalRoot() {
28        return doc.getElementById('baseWrap') || doc.body;
29    }
30
31    function mountList(wrap, list) {
32        if (!wrap || !list || list.getAttribute(PORTAL_ATTR) === 'true') return;
33        list.setAttribute(PORTAL_ATTR, 'true');
34        getPortalRoot().appendChild(list);
35    }
36
37    function unmountList(wrap, list) {
38        if (!wrap || !list || list.getAttribute(PORTAL_ATTR) !== 'true') return;
39        list.removeAttribute(PORTAL_ATTR);
40        wrap.appendChild(list);
41    }
42
43    function clearListPosition(list) {
44        if (!list) return;
45        list.style.top = '';
46        list.style.left = '';
47        list.style.width = '';
48    }
49
50    function positionList(btn, list) {
51        if (!btn || !list) return;
52        var rect = btn.getBoundingClientRect();
53        list.style.top = (rect.bottom + 4) + 'px';
54        list.style.left = rect.left + 'px';
55        list.style.width = rect.width + 'px';
56    }
57
58    function closeSelect(wrap) {
59        if (!wrap) return;
60        var btn = wrap.querySelector('.custom-select__btn');
61        var list = getList(wrap);
62        wrap.classList.remove(OPEN_CLASS);
63        if (btn) btn.setAttribute('aria-expanded', 'false');
64        if (list) {
65            list.hidden = true;
66            clearListPosition(list);
67            unmountList(wrap, list);
68        }
69    }
70
71    function closeAll(except) {
72        doc.querySelectorAll('.custom-select.' + OPEN_CLASS).forEach(function (wrap) {
73            if (except && wrap === except) return;
74            closeSelect(wrap);
75        });
76    }
77
78    function syncUiFromNative(wrap) {
79        var native = wrap.querySelector('.custom-select__native');
80        var btn = wrap.querySelector('.custom-select__btn');
81        var list = getList(wrap);
82        if (!native || !btn || !list) return;
83
84        var selected = native.options[native.selectedIndex];
85        var value = native.value;
86        var label = selected ? selected.text : '';
87        var isPlaceholder = !value;
88        var placeholder = wrap.getAttribute('data-placeholder') || '선택해 주세요';
89
90        btn.textContent = isPlaceholder ? placeholder : (label || placeholder);
91        btn.classList.toggle('is-placeholder', isPlaceholder);
92
93        list.querySelectorAll('.custom-select__item').forEach(function (item) {
94            var itemBtn = item.querySelector('button');
95            var isActive = !!(itemBtn && itemBtn.getAttribute('data-value') === value);
96            item.classList.toggle('custom-select__item--active', isActive);
97            if (itemBtn) {
98                if (isActive) itemBtn.setAttribute('title', '선택됨');
99                else itemBtn.removeAttribute('title');
100            }
101        });
102    }
103
104    function setNativeValue(wrap, value) {
105        var native = wrap.querySelector('.custom-select__native');
106        if (!native) return;
107        native.value = value;
108        try {
109            native.dispatchEvent(new Event('change', { bubbles: true }));
110        } catch (e) {
111            var evt = doc.createEvent('HTMLEvents');
112            evt.initEvent('change', true, false);
113            native.dispatchEvent(evt);
114        }
115        syncUiFromNative(wrap);
116    }
117
118    function buildListFromOptions(wrap, native, listId) {
119        var list = doc.createElement('ul');
120        list.id = listId;
121        list.className = 'custom-select__list';
122        list.hidden = true;
123        list.setAttribute('role', 'listbox');
124
125        Array.prototype.forEach.call(native.options, function (opt) {
126            var li = doc.createElement('li');
127            li.className = 'custom-select__item';
128            li.setAttribute('role', 'presentation');
129
130            var itemBtn = doc.createElement('button');
131            itemBtn.type = 'button';
132            itemBtn.setAttribute('role', 'option');
133            itemBtn.setAttribute('data-value', opt.value);
134            itemBtn.textContent = opt.text;
135
136            if (opt.disabled) {
137                itemBtn.disabled = true;
138            }
139
140            li.appendChild(itemBtn);
141            list.appendChild(li);
142        });
143
144        wrap.appendChild(list);
145        return list;
146    }
147
148    function ensureUi(wrap) {
149        var native = wrap.querySelector('select');
150        if (!native) return null;
151
152        native.classList.add('custom-select__native');
153
154        var id = native.id || ('custom-select-' + Math.random().toString(36).slice(2, 9));
155        if (!native.id) native.id = id;
156
157        var listId = id + '-list';
158        var btn = wrap.querySelector('.custom-select__btn');
159        var list = wrap.querySelector('.custom-select__list');
160
161        if (!btn) {
162            btn = doc.createElement('button');
163            btn.type = 'button';
164            btn.className = 'custom-select__btn';
165            btn.setAttribute('aria-haspopup', 'listbox');
166            btn.setAttribute('aria-expanded', 'false');
167            btn.setAttribute('aria-controls', listId);
168            wrap.appendChild(btn);
169        } else {
170            btn.setAttribute('aria-controls', listId);
171        }
172
173        if (!list) {
174            list = buildListFromOptions(wrap, native, listId);
175        } else if (!list.id) {
176            list.id = listId;
177        }
178
179        syncUiFromNative(wrap);
180        return { native: native, btn: btn, list: list };
181    }
182
183    function bindWrap(wrap) {
184        if (wrap.getAttribute(INIT_ATTR) === 'true') return;
185        var ui = ensureUi(wrap);
186        if (!ui) return;
187
188        var btn = ui.btn;
189        var list = ui.list;
190
191        btn.addEventListener('click', function () {
192            var isOpen = wrap.classList.contains(OPEN_CLASS);
193            closeAll();
194            if (isOpen) return;
195
196            wrap.classList.add(OPEN_CLASS);
197            btn.setAttribute('aria-expanded', 'true');
198            list.hidden = false;
199            mountList(wrap, list);
200            positionList(btn, list);
201        });
202
203        list.addEventListener('click', function (e) {
204            var optionBtn = e.target.closest('.custom-select__item > button');
205            if (!optionBtn || optionBtn.disabled) return;
206            setNativeValue(wrap, optionBtn.getAttribute('data-value') || '');
207            closeSelect(wrap);
208            btn.focus();
209        });
210
211        wrap.setAttribute(INIT_ATTR, 'true');
212    }
213
214    function repositionOpen() {
215        doc.querySelectorAll('.custom-select.' + OPEN_CLASS).forEach(function (wrap) {
216            var btn = wrap.querySelector('.custom-select__btn');
217            var list = getList(wrap);
218            positionList(btn, list);
219        });
220    }
221
222    var scrollListenersBound = false;
223    var lenisListenerBound = false;
224
225    function bindLenisScrollIfNeeded() {
226        if (lenisListenerBound) return;
227        if (win.__lenis && typeof win.__lenis.on === 'function') {
228            lenisListenerBound = true;
229            win.__lenis.on('scroll', repositionOpen);
230        }
231    }
232
233    function bindCustomSelectScrollListeners() {
234        if (!scrollListenersBound) {
235            scrollListenersBound = true;
236            win.addEventListener('resize', repositionOpen);
237
238            var baseWrap = doc.getElementById('baseWrap');
239            if (baseWrap) {
240                baseWrap.addEventListener('scroll', repositionOpen, { passive: true });
241            }
242            win.addEventListener('scroll', repositionOpen, { passive: true });
243        }
244
245        bindLenisScrollIfNeeded();
246        if (!lenisListenerBound) {
247            setTimeout(bindLenisScrollIfNeeded, 0);
248        }
249    }
250
251    function initCustomSelect2026(root) {
252        var scope = root && root.querySelectorAll ? root : doc;
253        scope.querySelectorAll('.custom-select').forEach(bindWrap);
254    }
255
256    doc.addEventListener('click', function (e) {
257        if (e.target.closest('.custom-select') || e.target.closest('.custom-select__list')) return;
258        closeAll();
259    });
260
261    doc.addEventListener('keydown', function (e) {
262        if (e.key === 'Escape' || e.keyCode === 27) {
263            closeAll();
264        }
265    });
266
267    function onCustomSelectReady() {
268        initCustomSelect2026();
269        bindCustomSelectScrollListeners();
270    }
271
272    if (doc.readyState === 'loading') {
273        doc.addEventListener('DOMContentLoaded', onCustomSelectReady);
274    } else {
275        onCustomSelectReady();
276    }
277
278    win.initCustomSelect2026 = initCustomSelect2026;
279})(window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.