diff --git a/ui_template/ui_template_find_list.ts b/ui_template/ui_template_find_list.ts index f4fcab62f..2be679a48 100644 --- a/ui_template/ui_template_find_list.ts +++ b/ui_template/ui_template_find_list.ts @@ -4,7 +4,7 @@ * 키보드 — 찾기 칸에서 ↑ ↓ 로 옮기고 Enter 로 고름. 창 겉틀은 부르는 쪽 몫. * * const find = createFindList({ placeholder, filters: [sub.root, detail.root] }); - * find.setRows([{ key, tag, name, sub, unit, onPick }]); + * find.setRows([{ key, tag, name, sub, unit, onPick }]); // 둘째 인자 = 처음 짚을 줄 key(가운데로 보임) * ========================================================================== */ import "./ui_template_find_list.css"; @@ -27,7 +27,8 @@ export interface FindList { root: HTMLElement; search: HTMLInputElement; hint: HTMLElement; - setRows: (rows: FindRow[]) => void; + /** active = 처음 짚을 줄 key(없으면 첫 줄) — 그 줄이 가운데로 보임 */ + setRows: (rows: FindRow[], active?: string) => void; } export function createFindList(opts: { @@ -42,12 +43,12 @@ export function createFindList(opts: { let rows: FindRow[] = []; let at = -1; - const mark = (i: number): void => { + const mark = (i: number, block: ScrollLogicalPosition = "nearest"): void => { list.children[at]?.classList.remove("is-active"); at = i; const row = list.children[at]; row?.classList.add("is-active"); - row?.scrollIntoView({ block: "nearest" }); + row?.scrollIntoView({ block }); }; field.input.addEventListener("keydown", (ev) => { if (ev.key === "ArrowDown" || ev.key === "ArrowUp") { @@ -61,7 +62,7 @@ export function createFindList(opts: { } }); - const setRows = (next: FindRow[]): void => { + const setRows = (next: FindRow[], active?: string): void => { rows = next; at = -1; list.replaceChildren( @@ -89,7 +90,9 @@ export function createFindList(opts: { return row; }), ); - if (rows.length) mark(0); + const pin = active === undefined ? -1 : rows.findIndex((r) => r.key === active); + if (pin >= 0) mark(pin, "center"); + else if (rows.length) mark(0); }; const filters = opts.filters?.length diff --git a/ui_template/ui_template_locale_machine.ts b/ui_template/ui_template_locale_machine.ts index e45ba5d55..b5376cb30 100644 --- a/ui_template/ui_template_locale_machine.ts +++ b/ui_template/ui_template_locale_machine.ts @@ -40,6 +40,7 @@ export const ui_locales_machine = { "National average · instant · server recalculates on save", ], MF_NoPrice: ["단가 없음", "no price"], + MF_PickFind: ["글자로 찾기", "Type to filter"], MF_Similar: ["비슷한 기계에서 시작", "Start from a similar machine"], MF_SimilarFind: ["기계 이름 · 규격으로 찾기", "Find by name or spec"], MF_SimilarNone: ["찾은 기계 없음", "No machines found"], @@ -51,6 +52,14 @@ export const ui_locales_machine = { M01_MachineMeta: ["원문 · 구분", "Source · group"], M01_MachineUses: ["쓰는 곳", "Used by"], M01_MachineNoUses: ["쓰는 곳 없음", "Not used"], + M01_MachineFormula: ["쓰는 계산식", "Hourly formula"], + M01_MachineFormulaListed: ["목록에 있음", "listed"], + M01_MachineFormulaShape: [ + "꼴로 고름(채운 칸) — 저장하면 그 식 목록에 등록", + "picked by filled fields — registered on save", + ], + M01_MachineFormulaOwn: ["그 기계만의 식", "machine's own formula"], + M01_MachineFormulaNone: ["식 없음", "no formula"], M01_MachineSame: [ "같은 이름 · 규격 기계가 있음 — {value}", "A machine with the same name and spec exists — {value}", diff --git a/ui_template/ui_template_machine_form.ts b/ui_template/ui_template_machine_form.ts index ac1c3019c..7060d5757 100644 --- a/ui_template/ui_template_machine_form.ts +++ b/ui_template/ui_template_machine_form.ts @@ -20,6 +20,7 @@ import { import { createCalcFormula, createCalcFormulaRow } from "./ui_template_calc_row"; import { el, createInputField, createSelectField } from "./ui_template_elements"; import { createFindList } from "./ui_template_find_list"; +import { attachPickSearch } from "./ui_template_pick_search"; import { t as L } from "./ui_template_locale"; import { createSegment } from "./ui_template_segment"; import { createText } from "./ui_template_text"; @@ -237,6 +238,7 @@ export function createMachineForm(opts: MachineFormOptions): MachineFormHandle { const field = createSelectField({ label: tr(label), options, value: now, size: 28 }); field.select.dataset.key = key; field.select.disabled = Boolean(opts.readOnly); + attachPickSearch(field.select, { placeholder: L("MF_PickFind") }); // 항목 많은 고르기(직종) = 찾기 판(801-2) const mark = (): void => { field.root.classList.toggle("is-changed", !same(values[key] ?? null, init[key] ?? null)); }; @@ -448,11 +450,11 @@ export function createMachineForm(opts: MachineFormOptions): MachineFormHandle { const rows = [ line(L("MF_Loss"), `${num("가격")}천원 × 1,000 × ${num("손료계수")} × 10^-7^`, p.손료), line(L("MF_Fuel"), `${num("주연료")}ℓ × ${fmt(fuel)}원`, p.주연료), - line(L("MF_Misc"), `${fmt(p.주연료)} × ${num("잡재료율")}%`, p.잡품), - line(L("MF_Crew"), `${num("조종원")}인 × ${fmt(wage)}원 × ${fmt(hour)}`, p.조종원), ...(values["전력"] ? [line(L("MF_Power"), `${num("전력")}kWh × ${fmt(opts.power ?? 0)}원`, p.전력)] : []), + line(L("MF_Misc"), `${fmt(p.주연료 + p.전력)} × ${num("잡재료율")}%`, p.잡품), + line(L("MF_Crew"), `${num("조종원")}인 × ${fmt(wage)}원 × ${fmt(hour)}`, p.조종원), ]; side.replaceChildren( ...head, diff --git a/ui_template/ui_template_pick_search.css b/ui_template/ui_template_pick_search.css new file mode 100644 index 000000000..097dc7c1b --- /dev/null +++ b/ui_template/ui_template_pick_search.css @@ -0,0 +1,12 @@ +/* 고르기 찾기 판(ui_template_pick_search.ts · 801-2) — 고르기 칸 바로 아래 · 목록 높이 제한 */ +.ui-pick-search-host { + position: relative; +} + +.ui-pick-search .ui-float-panel__body { + padding: var(--spacing-8); +} + +.ui-pick-search .ui-find__list { + max-height: 240px; +} diff --git a/ui_template/ui_template_pick_search.ts b/ui_template/ui_template_pick_search.ts new file mode 100644 index 000000000..9aa9b14f6 --- /dev/null +++ b/ui_template/ui_template_pick_search.ts @@ -0,0 +1,88 @@ +/* ============================================================================= + * 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(); + } + }); +}