- 카드 · 편집이 그 구조물의 복사본(폴더 = 구조물 id · 관 = pipe@측점)을 읽고 고침 · 같은 항목을 고른 구조물끼리 값이 따로 - [재적용](덮이는 것을 확인 창으로 알리고 덮임 부분을 알림) · [개인에 저장](경로 · 이름 물음) · 상태 줄에 고침 · 마스터가 바뀜 · 원본 없음 표시 - 나무 개인 그룹 · 폴더 오른쪽 클릭 「새 항목」 → 돌려받은 summary_item 을 폼에 지정 · [적용] 뒤 [저장] 안내 한 줄 - B05 구조물 폼이 구조물 id 를 모달에 넘김 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_item_picker_ask.ts
|
|
* 고르기 모달에서 글을 물을 때 쓰는 작은 입력 창(계획서 26-3b) — [개인에 저장](경로 · 이름) ·
|
|
* 나무 오른쪽 클릭 「새 항목」(이름 · 단위). 확인 = 넣은 글들 · 취소 · 닫기 = null.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, el } from "./ui_template_elements";
|
|
import { openModal } from "./ui_template_modal";
|
|
|
|
export interface AskField {
|
|
key: string;
|
|
label: string;
|
|
value?: string;
|
|
placeholder?: string;
|
|
/** 비우면 확인 안 됨 */
|
|
required?: boolean;
|
|
}
|
|
|
|
export interface AskOptions {
|
|
title: string;
|
|
/** 창 위 한 줄 설명 */
|
|
note?: string;
|
|
fields: AskField[];
|
|
okLabel: string;
|
|
cancelLabel: string;
|
|
closeLabel: string;
|
|
/** 빈 값 알림 — `{label}` 자리에 칸 이름 */
|
|
emptyText: string;
|
|
}
|
|
|
|
export function askFields(opts: AskOptions): Promise<Record<string, string> | null> {
|
|
return new Promise((resolve) => {
|
|
let done = false;
|
|
const finish = (value: Record<string, string> | null): void => {
|
|
if (done) return;
|
|
done = true;
|
|
resolve(value);
|
|
};
|
|
openModal({
|
|
title: opts.title,
|
|
closeLabel: opts.closeLabel,
|
|
dialogClass: "ui-item-ask",
|
|
backdropClass: "ui-item-ask__backdrop",
|
|
beforeClose: () => {
|
|
finish(null);
|
|
return true;
|
|
},
|
|
mount: (body, close) => {
|
|
const inputs = opts.fields.map((field) => ({
|
|
field,
|
|
input: createInputField({
|
|
label: field.label,
|
|
value: field.value ?? "",
|
|
placeholder: field.placeholder,
|
|
}),
|
|
}));
|
|
const submit = (): void => {
|
|
let bad = false;
|
|
for (const { field, input } of inputs) {
|
|
const empty = field.required && input.input.value.trim() === "";
|
|
input.setError(empty ? opts.emptyText.replace("{label}", field.label) : undefined);
|
|
bad ||= !!empty;
|
|
}
|
|
if (bad) return;
|
|
finish(
|
|
Object.fromEntries(
|
|
inputs.map(({ field, input }) => [field.key, input.input.value.trim()]),
|
|
),
|
|
);
|
|
close();
|
|
};
|
|
for (const { input } of inputs)
|
|
input.input.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") submit();
|
|
});
|
|
body.append(
|
|
...(opts.note ? [el("p", { className: "ui-item-picker__where", text: opts.note })] : []),
|
|
...inputs.map(({ input }) => input.root),
|
|
el("div", {
|
|
className: "ui-item-picker__actions",
|
|
children: [
|
|
createButton({
|
|
label: opts.cancelLabel,
|
|
variant: "ghost",
|
|
onClick: () => {
|
|
finish(null);
|
|
close();
|
|
},
|
|
}),
|
|
createButton({ label: opts.okLabel, variant: "filled", onClick: submit }),
|
|
],
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
});
|
|
}
|