/* ============================================================================= * ui_template_pick_search.ts * 고르기 칸에 찾기 붙이기(요구 801-2) — 항목이 많은 `createSelectField` 고르기를 열면 바로 아래에 * 찾기 판(팝오버 `createFloatPanel` + 찾기 목록 `createFindList` · 높이 제한 · 스크롤)이 뜸. * 글자를 치면 이름에 든 것만 · 찾기를 비우면 다시 전부 · 고른 항목이 있으면 그 줄 둘레로 열림 * 고름 = select 값 바꿈 + change 알림(부르는 쪽 코드 그대로) · Esc · 바깥 누름 = 닫음 * 항목이 적으면(`min` 이하) 아무것도 안 함 — 보통 고르기 그대로 * 쓰는 법 = attachPickSearch(field.select, { placeholder: "글자로 찾기" }) * ========================================================================== */ import { createFindList } from "./ui_template_find_list"; import { createFloatPanel } from "./ui_template_float_panel"; import "./ui_template_pick_search.css"; const squash = (s: string): string => s.replace(/\s+/g, "").toLowerCase(); export function attachPickSearch( select: HTMLSelectElement, opts: { placeholder: string; min?: number }, ): void { if (select.options.length <= (opts.min ?? 12)) return; const host = select.parentElement; if (!host) return; host.classList.add("ui-pick-search-host"); let panel: HTMLElement | null = null; const close = (): void => { panel?.remove(); panel = null; document.removeEventListener("pointerdown", outside, true); }; const outside = (ev: PointerEvent): void => { if (panel && !panel.contains(ev.target as Node) && ev.target !== select) close(); }; const open = (): void => { if (panel || select.disabled) return; const fp = createFloatPanel({ popover: true, width: Math.max(select.offsetWidth, 260) }); fp.root.classList.add("ui-pick-search"); fp.root.style.left = `${select.offsetLeft}px`; fp.root.style.top = `${select.offsetTop + select.offsetHeight + 2}px`; const find = createFindList({ placeholder: opts.placeholder }); fp.body.append(find.root); const all = [...select.options].map((o) => ({ value: o.value, text: o.text })); const show = (): void => { const q = squash(find.search.value); const rows = q ? all.filter((o) => squash(o.text).includes(q)) : all; find.setRows( rows.map((o) => ({ key: o.value, name: o.text, onPick: () => { select.value = o.value; select.dispatchEvent(new Event("change", { bubbles: true })); close(); select.focus(); }, })), q ? undefined : select.value, ); }; find.search.addEventListener("input", show); find.search.addEventListener("keydown", (ev) => { if (ev.key === "Escape") { ev.preventDefault(); close(); select.focus(); } }); host.append(fp.root); panel = fp.root; show(); find.search.focus(); document.addEventListener("pointerdown", outside, true); }; select.addEventListener("mousedown", (ev) => { ev.preventDefault(); if (panel) close(); else open(); }); select.addEventListener("keydown", (ev) => { if (["Enter", " ", "ArrowDown"].includes(ev.key)) { ev.preventDefault(); open(); } }); }