feat(m02-df): 입력 판 재료 방식 셋 · 자재 찾기 쪽 넘기기 · 다른 로직 입력 초안
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_InputPanel_Material.ts
|
||||
* 입력 판 — 재료 요소 방식 셋(고정 · 선택 자리 · 조건 찾기) · 자재 찾기(띄어쓰기 AND · 규격까지 · 쪽 넘기기 · 전체 수)
|
||||
* 서버 `/api/m02/df/materials` 를 먼저 부르고 아직 없으면 M01 `/pick?kind=price` 로 같은 모양을 만듦
|
||||
* 글 [한, 영] 짝은 아래 TEXT · 단추는 공용 작은 크기
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { fetchFiles, fetchSubs, type SubInfo } from "../../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import { cfValues } from "./M02_DF_Api";
|
||||
import type { CfElementRef, CfLogic, CfMaterialMode, CfMaterialRule } from "./M02_DF_Model";
|
||||
import {
|
||||
catchNote,
|
||||
checkMaterial,
|
||||
elementOf,
|
||||
matches,
|
||||
materialElement,
|
||||
pageRange,
|
||||
removeElementNamed,
|
||||
variablePills,
|
||||
type MatItem,
|
||||
} from "./M02_DF_InputPanel_Build";
|
||||
|
||||
const TEXT = {
|
||||
Chosen: ["넣은 재료 — 알약마다 이름 · 값 · 단위", "Chosen materials"],
|
||||
None: ["아직 고른 재료가 없음", "Nothing chosen yet"],
|
||||
Remove: ["빼기", "Remove"],
|
||||
Fixed: ["고정", "Fixed"],
|
||||
Choice: ["선택 자리", "Choice slot"],
|
||||
Rule: ["조건 찾기", "Find by rule"],
|
||||
FixedHelp: ["품목 하나를 못 박음", "One fixed item"],
|
||||
ChoiceHelp: [
|
||||
"자리만 두고 프로젝트에서 품목을 고름(기본 품목 하나)",
|
||||
"A slot — pick the item per project",
|
||||
],
|
||||
RuleHelp: [
|
||||
"변수 알약 값으로 재료를 잡음 — 예: 관경 알약 값이 규격에 들어간 흄관",
|
||||
"Find by a variable pill",
|
||||
],
|
||||
Search: ["찾는 말 — 띄어 쓰면 모두 들어간 것", "Search — all words must match"],
|
||||
AllSub: ["전체", "All"],
|
||||
SubLabel: ["구분", "Group"],
|
||||
DetailLabel: ["상세구분", "Detail"],
|
||||
Loading: ["찾는 중…", "Searching…"],
|
||||
NoRow: ["찾는 재료 없음", "Nothing found"],
|
||||
Total: ["전체 {n} 개 중 {a}~{b} · {p} / {m} 쪽", "{a}-{b} of {n} · page {p} / {m}"],
|
||||
Prev: ["이전", "Prev"],
|
||||
Next: ["다음", "Next"],
|
||||
SlotName: ["자리 이름(알약 이름)", "Slot name (pill name)"],
|
||||
Scope: ["범위", "Scope"],
|
||||
Word: ["찾는 말(범위 안에서)", "Word in scope"],
|
||||
Default: ["기본 품목", "Default item"],
|
||||
DefaultNone: ["아직 안 고름 — 아래 목록에서 눌러 고름", "Not chosen — pick one below"],
|
||||
DefaultClear: ["기본 품목 비움", "Clear"],
|
||||
RuleField: ["찾는 칸", "Field"],
|
||||
FieldSpec: ["규격", "Spec"],
|
||||
FieldName: ["이름", "Name"],
|
||||
RuleOp: ["조건", "Condition"],
|
||||
OpIn: ["에 알약 값이 들어 있음", "contains the pill value"],
|
||||
OpEq: ["이 알약 값과 같음", "equals the pill value"],
|
||||
RulePill: ["변수 알약", "Variable pill"],
|
||||
NoPill: ["변수 알약이 없음 — 변수 입력 상자를 먼저 만듦", "No variable pill yet"],
|
||||
Put: ["넣기", "Add"],
|
||||
Pick: ["넣기", "Add"],
|
||||
Preview: ["지금 잡히는 재료", "Items caught now"],
|
||||
Price: ["가장 싼 값 {p} {u}", "cheapest {p} {u}"],
|
||||
RuleLine: ["{f}{o} · 알약 「{n}」 = {v}", "{f} {o} · pill “{n}” = {v}"],
|
||||
ScopeLine: ["범위 {s}", "scope {s}"],
|
||||
Failed: ["불러오지 못함", "Could not load"],
|
||||
} as const;
|
||||
|
||||
type TKey = keyof typeof TEXT;
|
||||
const t = (key: TKey, v: Record<string, string | number> = {}): string =>
|
||||
Object.entries(v).reduce(
|
||||
(s, [k, x]) => s.replace(`{${k}}`, String(x)),
|
||||
TEXT[key][currentLanguageIndex] as string,
|
||||
);
|
||||
|
||||
const fmt = (n: number): string => Number(n.toPrecision(8)).toLocaleString("ko-KR");
|
||||
|
||||
/* ── 자재 찾기 ── */
|
||||
export interface MatQuery {
|
||||
q: string;
|
||||
sub?: string;
|
||||
detail?: string;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
export interface MatPage {
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
items: MatItem[];
|
||||
}
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/** 서버가 없으면 M01 값 줄에서 같은 모양으로 — 띄어쓰기 AND(이름 + 규격) · 쪽은 앞에서 잘라 줌 */
|
||||
async function fallback(q: MatQuery): Promise<MatPage> {
|
||||
const r = await getJson<{ items: Record<string, unknown>[] }>(
|
||||
`/m01/pick?${new URLSearchParams({ kind: "price", q: q.q, limit: "200" })}`,
|
||||
);
|
||||
const rows = r.items.filter((i) => !q.sub || i["구분"] === q.sub);
|
||||
const items = rows.map((i): MatItem => ({
|
||||
ref: String(i["ref"]),
|
||||
name: String(i["이름"] ?? ""),
|
||||
spec: String(i["규격"] ?? ""),
|
||||
unit: String(i["단위"] ?? ""),
|
||||
price: typeof i["값"] === "number" ? i["값"] : null,
|
||||
구분: String(i["구분"] ?? ""),
|
||||
}));
|
||||
const from = (q.page - 1) * q.size;
|
||||
return {
|
||||
total: items.length,
|
||||
page: q.page,
|
||||
size: q.size,
|
||||
items: items.slice(from, from + q.size),
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchMaterials(q: MatQuery): Promise<MatPage> {
|
||||
const params = new URLSearchParams({
|
||||
q: q.q,
|
||||
page: String(q.page),
|
||||
size: String(q.size),
|
||||
sub: q.sub ?? "",
|
||||
detail: q.detail ?? "",
|
||||
});
|
||||
try {
|
||||
return await getJson<MatPage>(`/m02/df/materials?${params}`);
|
||||
} catch {
|
||||
return fallback(q);
|
||||
}
|
||||
}
|
||||
|
||||
let subsCache: SubInfo[] | null = null;
|
||||
async function materialSubs(): Promise<SubInfo[]> {
|
||||
if (subsCache) return subsCache;
|
||||
const files = await fetchFiles("재료");
|
||||
const f = files.find((x) => x.file.includes("자재품목")) ?? files[0];
|
||||
subsCache = await fetchSubs({ file: f.file });
|
||||
return subsCache;
|
||||
}
|
||||
|
||||
/** 구분 · 상세구분 고르기 칸 — 바뀌면 onChange */
|
||||
function scopeBoxes(
|
||||
init: { sub: string; detail: string },
|
||||
onChange: () => void,
|
||||
): { root: HTMLElement; get(): { sub: string; detail: string } } {
|
||||
const state = { ...init };
|
||||
const all = { value: "", text: t("AllSub") };
|
||||
const subBox = createSelectField({
|
||||
options: [all],
|
||||
value: "",
|
||||
compact: true,
|
||||
ariaLabel: t("SubLabel"),
|
||||
onChange: (v) => ((state.sub = v), (state.detail = ""), fill(), onChange()),
|
||||
});
|
||||
const detailBox = createSelectField({
|
||||
options: [all],
|
||||
value: "",
|
||||
compact: true,
|
||||
ariaLabel: t("DetailLabel"),
|
||||
onChange: (v) => ((state.detail = v), onChange()),
|
||||
});
|
||||
let subs: SubInfo[] = [];
|
||||
const fill = (): void => {
|
||||
const d = subs.find((s) => s.name === state.sub)?.details ?? [];
|
||||
detailBox.root.hidden = !d.length;
|
||||
detailBox.setOptions([all, ...d.map((x) => ({ value: x, text: x }))], state.detail);
|
||||
};
|
||||
fill();
|
||||
void materialSubs().then(
|
||||
(s) => {
|
||||
subs = s;
|
||||
subBox.setOptions([all, ...s.map((x) => ({ value: x.name, text: x.name }))], state.sub);
|
||||
fill();
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
return {
|
||||
root: el("div", { className: "m02-dfip__bar", children: [subBox.root, detailBox.root] }),
|
||||
get: () => ({ ...state }),
|
||||
};
|
||||
}
|
||||
|
||||
/** 품목 찾기 목록 — 찾는 말 · 구분 · 쪽 넘기기 · 눌러 고르기 */
|
||||
function itemList(
|
||||
onPick: (item: MatItem) => void,
|
||||
base?: () => { sub: string; detail: string },
|
||||
): HTMLElement {
|
||||
const q = createInputField({ type: "search", placeholder: t("Search"), compact: true });
|
||||
const list = el("div", { className: "m02-dfip__list" });
|
||||
const info = el("span", { className: "m02-dfip__muted" });
|
||||
let page = 1;
|
||||
let seq = 0;
|
||||
const own = base ? null : scopeBoxes({ sub: "", detail: "" }, () => ((page = 1), void load()));
|
||||
const scope = (): { sub: string; detail: string } => (base ? base() : own!.get());
|
||||
const prev = createButton({
|
||||
label: t("Prev"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((page -= 1), void load()),
|
||||
});
|
||||
const next = createButton({
|
||||
label: t("Next"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((page += 1), void load()),
|
||||
});
|
||||
const load = async (): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
|
||||
try {
|
||||
const r = await searchMaterials({ q: q.input.value.trim(), ...scope(), page, size: 12 });
|
||||
if (mine !== seq) return;
|
||||
const range = pageRange(r.total, page, 12);
|
||||
info.textContent = t("Total", {
|
||||
n: r.total,
|
||||
a: range.from,
|
||||
b: range.to,
|
||||
p: Math.min(page, range.pages),
|
||||
m: range.pages,
|
||||
});
|
||||
prev.disabled = page <= 1;
|
||||
next.disabled = page >= range.pages;
|
||||
list.replaceChildren(
|
||||
...(r.items.length
|
||||
? r.items.map((it) => {
|
||||
const b = el("button", {
|
||||
className: "m02-dfip__row",
|
||||
attrs: { type: "button" },
|
||||
children: [
|
||||
el("span", {
|
||||
className: "m02-dfip__name",
|
||||
children: [
|
||||
el("strong", { text: it.name }),
|
||||
el("span", { className: "m02-dfip__muted", text: ` ${it.spec}` }),
|
||||
],
|
||||
}),
|
||||
el("span", {
|
||||
className: "m02-dfip__muted",
|
||||
text: `${it.price === null ? "" : fmt(it.price)} ${it.unit}`.trim(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
b.addEventListener("click", () => onPick(it));
|
||||
return b;
|
||||
})
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("NoRow") })]),
|
||||
);
|
||||
} catch {
|
||||
if (mine === seq)
|
||||
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Failed") }));
|
||||
}
|
||||
};
|
||||
let timer = 0;
|
||||
q.input.addEventListener(
|
||||
"input",
|
||||
() => (
|
||||
window.clearTimeout(timer),
|
||||
(timer = window.setTimeout(() => ((page = 1), void load()), 250))
|
||||
),
|
||||
);
|
||||
void load();
|
||||
return el("div", {
|
||||
className: "m02-dfip__find",
|
||||
children: [
|
||||
...(own ? [own.root] : []),
|
||||
q.root,
|
||||
info,
|
||||
list,
|
||||
el("div", { className: "m02-dfip__chips", children: [prev, next] }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 잡히는 재료 미리 보기 ── */
|
||||
export async function previewOf(
|
||||
el1: CfElementRef,
|
||||
lg: CfLogic,
|
||||
): Promise<{ found: number; names: string[]; cheapest: MatItem | null }> {
|
||||
const scope = el1.scope ?? {};
|
||||
const word = scope.찾는말 ?? "";
|
||||
if (el1.mode === "rule" && el1.rule) {
|
||||
const pill = variablePills(lg).find(
|
||||
(p) => p.port.box === el1.rule!.from.box && p.port.name === el1.rule!.from.name,
|
||||
);
|
||||
const value = pill?.example;
|
||||
if (value === null || value === undefined || value === "")
|
||||
return { found: 0, names: [], cheapest: null };
|
||||
const r = await searchMaterials({
|
||||
q: `${word} ${value}`.trim(),
|
||||
sub: scope.구분,
|
||||
detail: scope.상세구분,
|
||||
page: 1,
|
||||
size: 200,
|
||||
});
|
||||
const hit = r.items.filter((i) => matches(i, el1.rule!, value));
|
||||
return {
|
||||
found: hit.length,
|
||||
names: hit.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()),
|
||||
cheapest: cheapestOf(hit),
|
||||
};
|
||||
}
|
||||
const r = await searchMaterials({
|
||||
q: word,
|
||||
sub: scope.구분,
|
||||
detail: scope.상세구분,
|
||||
page: 1,
|
||||
size: 200,
|
||||
});
|
||||
return {
|
||||
found: r.total,
|
||||
names: r.items.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()),
|
||||
cheapest: cheapestOf(r.items),
|
||||
};
|
||||
}
|
||||
|
||||
const cheapestOf = (items: MatItem[]): MatItem | null =>
|
||||
items
|
||||
.filter((i) => i.price !== null)
|
||||
.sort((a, b) => (a.price as number) - (b.price as number))[0] ?? null;
|
||||
|
||||
/** 요소 한 줄 요약 — 방식 · 범위 · 규칙 */
|
||||
function summaryOf(e: CfElementRef, lg: CfLogic): string {
|
||||
const parts: string[] = [];
|
||||
const scope = Object.entries(e.scope ?? {})
|
||||
.map(([, v]) => v)
|
||||
.join(" / ");
|
||||
if (scope) parts.push(t("ScopeLine", { s: scope }));
|
||||
if (e.rule) {
|
||||
const p = variablePills(lg).find(
|
||||
(x) => x.port.box === e.rule!.from.box && x.port.name === e.rule!.from.name,
|
||||
);
|
||||
parts.push(
|
||||
t("RuleLine", {
|
||||
f: e.rule.field,
|
||||
o: e.rule.op === "포함" ? t("OpIn") : t("OpEq"),
|
||||
n: e.rule.from.name,
|
||||
v: p?.example ?? "—",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/** 재료 판 — 넣은 재료 목록 + 방식 셋 */
|
||||
export function renderMaterial(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
boxId: string,
|
||||
on: { change(lg: CfLogic): void },
|
||||
addEl: (lg: CfLogic, el: CfElementRef) => CfLogic,
|
||||
): void {
|
||||
const box = lg.boxes.find((b) => b.id === boxId);
|
||||
const els = (box?.input?.elements ?? []) as CfElementRef[];
|
||||
const chosen = el("div", { className: "m02-dfip__chosen" });
|
||||
const body = el("div", { className: "m02-dfip__stack" });
|
||||
let mode: CfMaterialMode = "fixed";
|
||||
|
||||
const drawChosen = (): void => {
|
||||
chosen.replaceChildren(
|
||||
el("h5", { text: `${t("Chosen")} · ${els.length}` }),
|
||||
...(els.length
|
||||
? els.map((e) => {
|
||||
const val = el("strong", { className: "m02-dfip__value", text: "—" });
|
||||
const note = el("span", { className: "m02-dfip__muted" });
|
||||
const kind =
|
||||
e.mode === "choice" ? t("Choice") : e.mode === "rule" ? t("Rule") : t("Fixed");
|
||||
const row = el("div", {
|
||||
className: "m02-dfip__item",
|
||||
children: [
|
||||
el("span", {
|
||||
className: "m02-dfip__name",
|
||||
children: [
|
||||
el("span", { className: "m02-dfip__tag", text: kind }),
|
||||
el("strong", { text: ` ${e.name}` }),
|
||||
el("div", { className: "m02-dfip__muted", text: summaryOf(e, lg) }),
|
||||
note,
|
||||
],
|
||||
}),
|
||||
val,
|
||||
el("span", { className: "m02-dfip__muted", text: e.unit || "-" }),
|
||||
createButton({
|
||||
label: t("Remove"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => on.change(removeElementNamed(lg, boxId, e.name)),
|
||||
}),
|
||||
],
|
||||
});
|
||||
const ref = e.mode === "choice" ? e.default : e.mode === "rule" ? "" : e.ref;
|
||||
if (ref)
|
||||
void cfValues([{ id: ref, group: "재료", ref }] as never)
|
||||
.then((a) =>
|
||||
a.forEach(
|
||||
(x) => x.ok && typeof x.value === "number" && (val.textContent = fmt(x.value)),
|
||||
),
|
||||
)
|
||||
.catch(() => undefined);
|
||||
if (e.mode && e.mode !== "fixed")
|
||||
void previewOf(e, lg).then(
|
||||
(p) => {
|
||||
const n = catchNote(p.found, p.names[0]);
|
||||
note.textContent = `${t("Preview")}: ${n.text}`;
|
||||
note.classList.toggle("is-bad", n.kind !== "one");
|
||||
if (p.cheapest?.price != null && !ref) val.textContent = fmt(p.cheapest.price);
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
return row;
|
||||
})
|
||||
: [el("p", { className: "m02-dfip__muted", text: t("None") })]),
|
||||
);
|
||||
};
|
||||
|
||||
const chips = el("div", { className: "m02-dfip__chips" });
|
||||
const drawChips = (): void =>
|
||||
chips.replaceChildren(
|
||||
...(["fixed", "choice", "rule"] as CfMaterialMode[]).map((m) =>
|
||||
createButton({
|
||||
label: m === "fixed" ? t("Fixed") : m === "choice" ? t("Choice") : t("Rule"),
|
||||
variant: mode === m ? "filled" : "pill",
|
||||
size: "sm",
|
||||
onClick: () => ((mode = m), drawChips(), drawBody()),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const drawBody = (): void => {
|
||||
if (mode === "fixed") {
|
||||
body.replaceChildren(
|
||||
el("p", { className: "m02-dfip__muted", text: t("FixedHelp") }),
|
||||
itemList((it) =>
|
||||
on.change(
|
||||
addEl(lg, {
|
||||
...elementOf("재료", { 키: it.ref, 이름: it.name, 규격: it.spec, 단위: it.unit }),
|
||||
mode: "fixed",
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 선택 자리 · 조건 찾기 — 이름 · 범위 · (기본 품목 | 규칙) · 미리 보기
|
||||
const name = createInputField({
|
||||
type: "text",
|
||||
label: t("SlotName"),
|
||||
compact: true,
|
||||
placeholder: "예: 흄관",
|
||||
});
|
||||
const word = createInputField({ type: "text", label: t("Word"), compact: true });
|
||||
let def: MatItem | null = null;
|
||||
const scope = scopeBoxes({ sub: "", detail: "" }, () => void refresh());
|
||||
const note = el("p", { className: "m02-dfip__note" });
|
||||
const pills = variablePills(lg);
|
||||
const fieldBox = createSelectField({
|
||||
label: t("RuleField"),
|
||||
options: [
|
||||
{ value: "규격", text: t("FieldSpec") },
|
||||
{ value: "이름", text: t("FieldName") },
|
||||
],
|
||||
value: "규격",
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const opBox = createSelectField({
|
||||
label: t("RuleOp"),
|
||||
options: [
|
||||
{ value: "포함", text: t("OpIn") },
|
||||
{ value: "같음", text: t("OpEq") },
|
||||
],
|
||||
value: "포함",
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const pillBox = createSelectField({
|
||||
label: t("RulePill"),
|
||||
options: pills.map((p, i) => ({
|
||||
value: String(i),
|
||||
text: `${p.port.name}${p.example !== null ? ` = ${p.example}` : ""}`,
|
||||
})),
|
||||
value: "0",
|
||||
compact: true,
|
||||
onChange: () => void refresh(),
|
||||
});
|
||||
const defNote = el("p", { className: "m02-dfip__muted" });
|
||||
const build = (): CfElementRef => {
|
||||
const sc = scope.get();
|
||||
const rule: CfMaterialRule | undefined =
|
||||
mode === "rule" && pills[Number(pillBox.select.value)]
|
||||
? {
|
||||
field: fieldBox.select.value as CfMaterialRule["field"],
|
||||
op: opBox.select.value as CfMaterialRule["op"],
|
||||
from: pills[Number(pillBox.select.value)].port,
|
||||
}
|
||||
: undefined;
|
||||
return materialElement(
|
||||
mode,
|
||||
name.input.value,
|
||||
{ 구분: sc.sub, 상세구분: sc.detail, 찾는말: word.input.value.trim() },
|
||||
{ rule, item: def ?? undefined },
|
||||
);
|
||||
};
|
||||
let seq = 0;
|
||||
const refresh = async (): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
defNote.textContent = def ? `${t("Default")}: ${def.name} ${def.spec}` : t("DefaultNone");
|
||||
const cand = build();
|
||||
note.textContent = `${t("Preview")}: …`;
|
||||
try {
|
||||
const p = await previewOf(cand, lg);
|
||||
if (mine !== seq) return;
|
||||
const n = catchNote(p.found, p.names[0]);
|
||||
note.textContent = `${t("Preview")}: ${n.text}${p.cheapest?.price != null ? ` · ${t("Price", { p: fmt(p.cheapest.price), u: p.cheapest.unit })}` : ""}`;
|
||||
note.className = `m02-dfip__note${n.kind === "one" ? "" : " is-bad"}`;
|
||||
} catch {
|
||||
if (mine === seq) note.textContent = t("Failed");
|
||||
}
|
||||
};
|
||||
name.input.addEventListener("input", () => name.setError(undefined));
|
||||
word.input.addEventListener("input", () => void refresh());
|
||||
const add = createButton({
|
||||
label: t("Put"),
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const cand = build();
|
||||
const why = checkMaterial(cand, els);
|
||||
name.setError(why ?? undefined);
|
||||
if (why) return void showToast(why, "error");
|
||||
on.change(addEl(lg, cand));
|
||||
},
|
||||
});
|
||||
const clear = createButton({
|
||||
label: t("DefaultClear"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => ((def = null), void refresh()),
|
||||
});
|
||||
body.replaceChildren(
|
||||
el("p", {
|
||||
className: "m02-dfip__muted",
|
||||
text: mode === "choice" ? t("ChoiceHelp") : t("RuleHelp"),
|
||||
}),
|
||||
name.root,
|
||||
el("h5", { text: t("Scope") }),
|
||||
scope.root,
|
||||
word.root,
|
||||
...(mode === "rule"
|
||||
? pills.length
|
||||
? [fieldBox.root, opBox.root, pillBox.root]
|
||||
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]
|
||||
: [
|
||||
el("h5", { text: t("Default") }),
|
||||
defNote,
|
||||
clear,
|
||||
itemList((it) => ((def = it), void refresh())),
|
||||
]),
|
||||
note,
|
||||
add,
|
||||
);
|
||||
void refresh();
|
||||
};
|
||||
|
||||
host.replaceChildren(chosen, chips, body);
|
||||
drawChosen();
|
||||
drawChips();
|
||||
drawBody();
|
||||
}
|
||||
Reference in New Issue
Block a user