자주사용하는 것들 - 자동완성 선택기

2026.01.28 13:48

<SELECT ..></SELECT>에서 출력되는 것이 많을때

<form id="myForm" method="post">
  <div class="autocomplete_select" data-form-id="myForm">
    <button type="button" class="autocomplete_select__btn" id="autocomplete_select_btn">
      선택하세요
    </button>

    <div class="autocomplete_select__panel" id="autocomplete_select_panel">
      <input type="text" class="autocomplete_select__search" id="autocomplete_select_search" placeholder="검색">
      <div class="autocomplete_select__list" id="autocomplete_select_list"></div>
    </div>

    <!-- 선택 결과를 담을 hidden -->
    <input type="hidden" name="selected_id" id="autocomplete_select_value" value="">
    <input type="hidden" name="selected_meta1" id="autocomplete_select_meta1" value="">
    <input type="hidden" name="selected_meta2" id="autocomplete_select_meta2" value="">
  </div>
</form>
.autocomplete_select{ position:relative; display:inline-block; min-width:140px; }
.autocomplete_select__btn{
  width:100%;
  padding:6px 10px;
  border:1px solid #cfcfcf;
  border-radius:8px;
  background:#fff;
  cursor:pointer;
  text-align:left;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

.autocomplete_select__panel{
  display:none;
  width:320px;
  border:1px solid #cfcfcf;
  border-radius:10px;
  background:#fff;
  padding:8px;
  box-shadow:0 6px 18px rgba(0,0,0,.08);
  box-sizing:border-box;
  z-index:99999;
}

.autocomplete_select__search{
  width:100%;
  padding:6px 8px;
  border:1px solid #cfcfcf;
  border-radius:8px;
  box-sizing:border-box;
}

.autocomplete_select__list{
  margin-top:8px;
  max-height:240px;
  overflow:auto;
  border:1px solid #e9e9e9;
  border-radius:8px;
}

.autocomplete_select__item{
  padding:7px 9px;
  cursor:pointer;
  border-bottom:1px solid #f0f0f0;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.autocomplete_select__item:last-child{ border-bottom:none; }
.autocomplete_select__item:hover{ background:#f7f7f7; }

.autocomplete_select__muted{ color:#777; padding:8px; }
/** 데이터만 바꿔서 재사용 */
window.AUTOCOMPLETE_SELECT_DATA = window.AUTOCOMPLETE_SELECT_DATA || [
  { id: 'all', label: '전체', meta1: '', meta2: '' },
  // { id: '1', label: '예시 항목', meta1: 'x', meta2: 'y' },
];

(function autocomplete_select_init(opts){
  const cfg = Object.assign({
    rootSelector: '.autocomplete_select',
    btnId: 'autocomplete_select_btn',
    panelId: 'autocomplete_select_panel',
    searchId: 'autocomplete_select_search',
    listId: 'autocomplete_select_list',

    valueId: 'autocomplete_select_value',
    meta1Id: 'autocomplete_select_meta1',
    meta2Id: 'autocomplete_select_meta2',

    data: window.AUTOCOMPLETE_SELECT_DATA,
    panelWidth: 320,
    gap: 6,
    autoSubmit: true,
  }, opts || {});

  const root  = document.querySelector(cfg.rootSelector);
  if(!root) return;

  const formId = root.dataset.formId || '';
  const form = formId ? document.getElementById(formId) : root.closest('form');

  const btn   = document.getElementById(cfg.btnId);
  const panel = document.getElementById(cfg.panelId);
  const input = document.getElementById(cfg.searchId);
  const list  = document.getElementById(cfg.listId);

  const hidVal  = document.getElementById(cfg.valueId);
  const hidM1   = document.getElementById(cfg.meta1Id);
  const hidM2   = document.getElementById(cfg.meta2Id);

  if(!btn || !panel || !input || !list || !hidVal) return;

  // panel을 body로 떼서 fixed로 띄우면 overflow 영향을 덜 받음
  if(panel.parentElement !== document.body){
    document.body.appendChild(panel);
  }

  function place(){
    const r = btn.getBoundingClientRect();
    let left = r.left;
    const maxLeft = window.innerWidth - cfg.panelWidth - 8;
    if(left > maxLeft) left = Math.max(8, maxLeft);
    if(left < 8) left = 8;

    panel.style.position = 'fixed';
    panel.style.left = left + 'px';
    panel.style.top  = (r.bottom + cfg.gap) + 'px';
    panel.style.width = cfg.panelWidth + 'px';
  }

  function render(keyword){
    list.innerHTML = '';
    const kw = (keyword || '').toLowerCase().trim();

    const filtered = (cfg.data || []).filter(it =>
      String(it.label || '').toLowerCase().includes(kw)
    );

    if(filtered.length === 0){
      const div = document.createElement('div');
      div.className = 'autocomplete_select__muted';
      div.textContent = '검색 결과가 없습니다.';
      list.appendChild(div);
      return;
    }

    filtered.forEach(it => {
      const div = document.createElement('div');
      div.className = 'autocomplete_select__item';
      div.textContent = it.label;

      div.addEventListener('click', function(e){
        e.preventDefault();
        e.stopPropagation();

        hidVal.value = it.id ?? '';
        if(hidM1) hidM1.value = it.meta1 ?? '';
        if(hidM2) hidM2.value = it.meta2 ?? '';

        btn.textContent = it.label || '선택됨';

        panel.style.display = 'none';

        if(cfg.autoSubmit && form) form.submit();
      });

      list.appendChild(div);
    });
  }

  function openPanel(){
    place();
    panel.style.display = 'block';
    render(input.value);
    setTimeout(() => input.focus(), 0);
  }
  function closePanel(){ panel.style.display = 'none'; }

  btn.addEventListener('click', function(e){
    e.preventDefault();
    e.stopPropagation();
    if(panel.style.display === 'block') closePanel();
    else openPanel();
  });

  btn.addEventListener('keydown', function(e){
    if(e.key === 'Enter'){
      e.preventDefault();
      e.stopPropagation();
      if(panel.style.display === 'block') closePanel();
      else openPanel();
    }
  });

  input.addEventListener('input', function(){ render(this.value); });
  panel.addEventListener('click', e => e.stopPropagation());

  document.addEventListener('click', function(e){
    if(e.target !== btn && !panel.contains(e.target)) closePanel();
  });

  window.addEventListener('scroll', function(){ if(panel.style.display === 'block') place(); }, true);
  window.addEventListener('resize', function(){ if(panel.style.display === 'block') place(); });

  closePanel();
})();

댓글 0

아직 댓글이 없습니다.