feat(801-2): 기계 양식 직종 고르기에 찾기 판 · 쓰는 계산식 글 · 미리보기 잡품 글
- 부품 ui_template_pick_search(새) — 항목 많은 고르기(열둘 넘음)를 열면 찾기 판(팝오버 + 찾기 목록 · 높이 240 · 스크롤) · 글자로 거름 · 비우면 전부 · 고른 항목 둘레로 열림 · Esc · 바깥 누름 닫음 · 연료처럼 적은 고르기는 그대로 - 찾기 목록 setRows 둘째 인자 = 처음 짚을 줄(가운데로) - 기계 양식(M01 상세 · 내 기계 · 줄 쌓기 새 기계 셋 다): 직종 고르기에 붙임 · 전력 줄을 잡품 위로 · 잡품 = (주연료 + 전력) × % - 글: 쓰는 계산식 · 목록에 있음 · 꼴로 고름 · 글자로 찾기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user