172 lines
5.7 KiB
JavaScript
172 lines
5.7 KiB
JavaScript
|
|
// 기본 실행 (페이지 로드시 전체 document)
|
|
class IpAutoComplete {
|
|
/**
|
|
* @param {HTMLElement|string} input
|
|
* @param {HTMLElement|string} panel
|
|
* @param {string[]} ipArray
|
|
* @param {object} opts
|
|
*/
|
|
constructor(input, panel, ipArray = [], opts = {}) {
|
|
this.inputEl = typeof input === 'string' ? document.querySelector(input) : input;
|
|
this.panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
|
|
if (!this.inputEl || !this.panelEl) throw new Error('Invalid input/panel element');
|
|
|
|
this.opts = Object.assign({
|
|
validateIPv4: true,
|
|
enforceListOnly: false,
|
|
previewOnFocus: true, // 포커스 시 상위 N개 미리보기
|
|
previewCount: 30, // 미리보기 개수
|
|
resultLimit: 1000 // 너무 많을 때 렌더 최대치(성능 안전장치)
|
|
}, opts);
|
|
|
|
this.errorEl = document.getElementById('ipError');
|
|
this.ipv4Re = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
|
|
this.setArray(ipArray);
|
|
|
|
// 상태
|
|
this.activeIndex = -1;
|
|
|
|
// 이벤트
|
|
this.onInput = this._handleInput.bind(this);
|
|
this.onChange = this._handleChange.bind(this);
|
|
this.onFocus = this._handleFocus.bind(this);
|
|
this.onKeydown = this._handleKeydown.bind(this);
|
|
this.onClickOutside = this._handleClickOutside.bind(this);
|
|
|
|
this.inputEl.addEventListener('input', this.onInput);
|
|
this.inputEl.addEventListener('change', this.onChange);
|
|
this.inputEl.addEventListener('focus', this.onFocus);
|
|
this.inputEl.addEventListener('keydown', this.onKeydown);
|
|
document.addEventListener('mousedown', this.onClickOutside);
|
|
}
|
|
|
|
destroy(){
|
|
this.inputEl.removeEventListener('input', this.onInput);
|
|
this.inputEl.removeEventListener('change', this.onChange);
|
|
this.inputEl.removeEventListener('focus', this.onFocus);
|
|
this.inputEl.removeEventListener('keydown', this.onKeydown);
|
|
document.removeEventListener('mousedown', this.onClickOutside);
|
|
this.hidePanel();
|
|
}
|
|
|
|
setArray(arr){ this.ipArray = Array.from(new Set((arr||[]).filter(Boolean))); }
|
|
|
|
_handleInput(e){
|
|
const q = (e.target.value||'').trim();
|
|
this._filterAndRender(q);
|
|
this._checkIPv4(q);
|
|
}
|
|
|
|
_handleChange(){
|
|
const v = (this.inputEl.value||'').trim();
|
|
this._checkIPv4(v);
|
|
if (this.opts.enforceListOnly && v && !this.ipArray.includes(v)) {
|
|
this._setError('❌ 목록에 없는 IP입니다.');
|
|
}
|
|
}
|
|
|
|
_handleFocus(){
|
|
if (!this.inputEl.value && this.opts.previewOnFocus) {
|
|
this._renderPanel(this.ipArray.slice(0, this.opts.resultLimit));
|
|
this.showPanel();
|
|
}
|
|
}
|
|
|
|
_handleKeydown(e){
|
|
if (this.panelEl.hidden) return;
|
|
const items = this._items();
|
|
if (!items.length) return;
|
|
|
|
if (e.key === 'ArrowDown'){ e.preventDefault(); this._move(1); }
|
|
else if (e.key === 'ArrowUp'){ e.preventDefault(); this._move(-1); }
|
|
else if (e.key === 'Enter'){
|
|
if (this.activeIndex >= 0){ e.preventDefault(); this._pick(items[this.activeIndex].dataset.value); }
|
|
} else if (e.key === 'Escape'){ this.hidePanel(); }
|
|
}
|
|
|
|
_handleClickOutside(e){
|
|
if (!this.inputEl.closest('.ac-wrap')?.contains(e.target)) this.hidePanel();
|
|
}
|
|
|
|
_filterAndRender(query){
|
|
if (!query){
|
|
this.hidePanel();
|
|
return;
|
|
}
|
|
const q = query.toLowerCase();
|
|
let results = this.ipArray.filter(ip => ip.toLowerCase().includes(q));
|
|
if (this.opts.resultLimit > 0) results = results.slice(0, this.opts.resultLimit);
|
|
if (results.length){ this._renderPanel(results); this.showPanel(); }
|
|
else { this.hidePanel(); }
|
|
}
|
|
|
|
_renderPanel(list){
|
|
this.panelEl.innerHTML = '';
|
|
this.activeIndex = -1;
|
|
|
|
const frag = document.createDocumentFragment();
|
|
list.forEach((ip) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'ac-item';
|
|
item.textContent = ip;
|
|
item.dataset.value = ip;
|
|
item.addEventListener('mousedown', (ev) => { ev.preventDefault(); this._pick(ip); });
|
|
frag.appendChild(item);
|
|
});
|
|
this.panelEl.appendChild(frag);
|
|
}
|
|
|
|
_items(){ return Array.from(this.panelEl.querySelectorAll('.ac-item')); }
|
|
|
|
_move(delta){
|
|
const items = this._items();
|
|
if (!items.length) return;
|
|
this.activeIndex = (this.activeIndex + delta + items.length) % items.length;
|
|
items.forEach(el => el.classList.remove('active'));
|
|
const el = items[this.activeIndex];
|
|
el.classList.add('active');
|
|
el.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
_pick(value){
|
|
this.inputEl.value = value;
|
|
this.hidePanel();
|
|
this._checkIPv4(value);
|
|
}
|
|
|
|
showPanel(){ this.panelEl.hidden = false; }
|
|
hidePanel(){ this.panelEl.hidden = true; this.activeIndex = -1; }
|
|
|
|
_checkIPv4(val){
|
|
if (!this.opts.validateIPv4 || !val){ this._setError(''); return; }
|
|
this._setError(this.ipv4Re.test(val) ? '' : '❌ IPv4 형식이 아닙니다.');
|
|
}
|
|
|
|
_setError(text){ if (this.errorEl) this.errorEl.textContent = text || ''; }
|
|
}
|
|
|
|
/* ipData(JSON) 읽기: (이미 페이지에 있다면) */
|
|
function parseEmbeddedJson(id){
|
|
const el=document.getElementById(id); if(!el) return [];
|
|
let txt=el.textContent||'';
|
|
txt = txt
|
|
.replace(/\/\*[\s\S]*?\*\//g,'')
|
|
.replace(/(^|[^:])\/\/.*$/mg,'$1')
|
|
.replace(/,\s*([}\]])/g,'$1');
|
|
try { const data=JSON.parse(txt); return Array.isArray(data)?data:[]; }
|
|
catch(e){ console.error('ipData parse error',e); return []; }
|
|
}
|
|
|
|
/* 초기화 */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// ipData <script>에 서버가 넣어준 배열 사용 (API 호출 없음)
|
|
const ips = parseEmbeddedJson('ipData');
|
|
const ac = new IpAutoComplete('#ipInput', '#ipPanel', ips, {
|
|
validateIPv4: true,
|
|
enforceListOnly: false,
|
|
previewOnFocus: true,
|
|
previewCount: 30,
|
|
resultLimit: 2000 // 너무 많으면 1000~2000 정도로 제한 추천
|
|
});
|
|
}); |