- 값 바꿀 때 고름 길(pickAt)을 click · 키보드 초점에도 씀 — 칸 제 동작 뒤 setTimeout - 고름이 폼을 다시 그려 초점 칸이 사라지면 같은 이름 새 칸에 초점 되살림 - 시험 test_191_1_box_press_pick + 흉내 DOM 헬퍼 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
234 lines
7.0 KiB
JavaScript
234 lines
7.0 KiB
JavaScript
/* 191-1 좌측 세부 컨테이너 누름 · 초점 = 형상 고름 — 실제 `wireStructureSelection` 을 그 자리에서
|
|
* 트랜스파일해 흉내 DOM 에서 돌림. 고름(select)이 폼을 다시 그리는 것까지 흉내. 짝 = `test_191_1_box_press_pick.py`.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
class FakeNode {
|
|
constructor(tag, dataset = {}) {
|
|
this.tagName = tag.toUpperCase();
|
|
this.dataset = dataset;
|
|
this.children = [];
|
|
this.parentElement = null;
|
|
this._listeners = {};
|
|
}
|
|
append(...kids) {
|
|
kids.forEach((k) => {
|
|
k.parentElement = this;
|
|
this.children.push(k);
|
|
});
|
|
}
|
|
replaceChildren(...kids) {
|
|
this.children.forEach((k) => (k.parentElement = null));
|
|
this.children = [];
|
|
this.append(...kids);
|
|
}
|
|
addEventListener(type, fn) {
|
|
(this._listeners[type] ??= []).push(fn);
|
|
}
|
|
matches(sel) {
|
|
return sel.split(",").some((s) => {
|
|
const m = s.trim().match(/^(\w+)?(?:\[data-([\w-]+)(?:="([^"]*)")?\])?$/);
|
|
if (!m) return false;
|
|
const [, tag, attr, value] = m;
|
|
if (tag && this.tagName !== tag.toUpperCase()) return false;
|
|
if (!attr) return true;
|
|
const key = attr.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
return value === undefined ? this.dataset[key] !== undefined : this.dataset[key] === value;
|
|
});
|
|
}
|
|
closest(sel) {
|
|
for (let n = this; n; n = n.parentElement) if (n.matches?.(sel)) return n;
|
|
return null;
|
|
}
|
|
contains(node) {
|
|
for (let n = node; n; n = n.parentElement) if (n === this) return true;
|
|
return false;
|
|
}
|
|
get isConnected() {
|
|
return panelRoot.contains(this);
|
|
}
|
|
querySelectorAll(sel) {
|
|
const found = [];
|
|
const walk = (n) =>
|
|
n.children.forEach((k) => {
|
|
if (k.matches(sel)) found.push(k);
|
|
walk(k);
|
|
});
|
|
walk(this);
|
|
return found;
|
|
}
|
|
focus() {
|
|
globalThis.document.activeElement = this;
|
|
}
|
|
}
|
|
// 흉내 이벤트 — 대상에서 뿌리까지 거품
|
|
function fire(target, type) {
|
|
for (let n = target; n; n = n.parentElement)
|
|
(n._listeners[type] ?? []).forEach((fn) => fn({ type, target }));
|
|
}
|
|
|
|
globalThis.document = { activeElement: null };
|
|
const timers = [];
|
|
globalThis.setTimeout = (fn) => timers.push(fn);
|
|
const flush = () => {
|
|
while (timers.length) timers.shift()();
|
|
};
|
|
globalThis.queueMicrotask = (fn) => fn();
|
|
|
|
// 좌측 폼 — 상자 넷(유입 · 유출 · 관 · 흙막이 둘) · 칸마다 data-spec-item 입력 하나
|
|
const BOXES = [
|
|
["inlet", undefined, "inlet.kind"],
|
|
["outlet", undefined, "outlet.tiers"],
|
|
["pipe", undefined, "pipe.diameter"],
|
|
["soil_add", "0", "soil.height"],
|
|
["soil_add", "1", "soil.height"],
|
|
];
|
|
const panelRoot = new FakeNode("div");
|
|
let paints = 0;
|
|
function paint() {
|
|
paints += 1;
|
|
panelRoot.replaceChildren(
|
|
...BOXES.map(([id, index, item]) => {
|
|
const box = new FakeNode("fieldset", { specContainer: id, specIndex: index });
|
|
const legend = new FakeNode("legend");
|
|
const row = new FakeNode("label", { specItem: item });
|
|
const input = new FakeNode("input");
|
|
row.append(input);
|
|
box.append(legend, row);
|
|
return box;
|
|
}),
|
|
);
|
|
}
|
|
paint();
|
|
const boxOf = (id, index) =>
|
|
panelRoot
|
|
.querySelectorAll("fieldset")
|
|
.find((b) => b.dataset.specContainer === id && b.dataset.specIndex === index);
|
|
const inputOf = (id, index) => boxOf(id, index).querySelectorAll("input")[0];
|
|
|
|
const CH = 120;
|
|
const SECTION = { chainage_m: CH, culvert: {} };
|
|
let highlight = null;
|
|
const log = [];
|
|
const stationControls = {
|
|
structureSpan: { ownerOf: () => SECTION },
|
|
inletStructure: { set: () => {}, valueFor: () => "auto" },
|
|
revetOffset: {
|
|
select: (chainageM, key) => {
|
|
highlight = key;
|
|
log.push(["select", chainageM, key]);
|
|
},
|
|
highlightFor: () => highlight,
|
|
},
|
|
ford: { select: () => {} },
|
|
box: { select: () => {} },
|
|
};
|
|
const panel = {
|
|
root: panelRoot,
|
|
currentChainage: () => CH,
|
|
showAtChainage: () => paint(), // 고름이 폼을 다시 그림 — 누른 칸이 사라짐
|
|
releasePipe: () => {},
|
|
facility: { onInletStructureChange: () => {}, setInletStructure: () => {} },
|
|
};
|
|
|
|
function loadSelection() {
|
|
const file = path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Selection.ts");
|
|
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const stubs = { "./B06_Section_UI_Adjust_Dock": { refreshAdjustSlots: () => {} } };
|
|
new Function("exports", "module", "require", js)(box.exports, box, (r) => stubs[r]);
|
|
return box.exports;
|
|
}
|
|
loadSelection().wireStructureSelection(
|
|
stationControls,
|
|
() => ({ cross_sections: [SECTION] }),
|
|
panel,
|
|
(chainageM, key) => log.push(["remember", chainageM, key]),
|
|
(chainageM) => log.push(["card", chainageM]),
|
|
(chainageM) => log.push(["reveal", chainageM]),
|
|
);
|
|
|
|
const reset = () => {
|
|
highlight = null;
|
|
log.length = 0;
|
|
document.activeElement = null;
|
|
};
|
|
// 마우스 누름 — pointerdown · (칸이면 초점) · click
|
|
function press(target, focus = false) {
|
|
fire(target, "pointerdown");
|
|
if (focus) {
|
|
target.focus();
|
|
fire(target, "focusin");
|
|
}
|
|
fire(target, "click");
|
|
flush();
|
|
}
|
|
const out = {};
|
|
|
|
// 값 바꿈(지금 길) — 기준
|
|
reset();
|
|
inputOf("inlet").focus();
|
|
fire(inputOf("inlet"), "change");
|
|
out.change = { highlight, log: [...log] };
|
|
|
|
// 빈 곳 · 제목 누름 = 같은 결과
|
|
reset();
|
|
press(boxOf("inlet"));
|
|
out.pressBox = { highlight, log: [...log] };
|
|
reset();
|
|
press(boxOf("inlet").children[0]);
|
|
out.pressLegend = { highlight, log: [...log] };
|
|
|
|
// 입력 칸 누름(초점) — 고름 + 다시 그린 새 칸에 초점
|
|
reset();
|
|
const before = paints;
|
|
const old = inputOf("pipe");
|
|
press(old, true);
|
|
out.pressInput = {
|
|
highlight,
|
|
repainted: paints > before,
|
|
oldConnected: old.isConnected,
|
|
focusSame: document.activeElement === inputOf("pipe"),
|
|
};
|
|
|
|
// 키보드(Tab) 초점 = 고름 · 다른 상자 누르면 바뀜
|
|
reset();
|
|
const tabbed = inputOf("soil_add", "1");
|
|
fire(tabbed, "keydown");
|
|
tabbed.focus();
|
|
fire(tabbed, "focusin");
|
|
flush();
|
|
out.tab = { highlight, focusSame: document.activeElement === inputOf("soil_add", "1") };
|
|
press(boxOf("soil_add", "0"));
|
|
out.switched = highlight;
|
|
press(boxOf("outlet"));
|
|
out.switched2 = highlight;
|
|
|
|
// 누름이 준 초점은 click 전엔 안 고름(누른 칸을 click 전에 지우지 않음)
|
|
reset();
|
|
const target = inputOf("pipe");
|
|
fire(target, "pointerdown");
|
|
target.focus();
|
|
fire(target, "focusin");
|
|
flush();
|
|
out.focusBeforeClick = { highlight, connected: target.isConnected };
|
|
|
|
// 단 수 칸 누름 = 값 바꿀 때처럼 안 고름
|
|
reset();
|
|
press(inputOf("outlet"), true);
|
|
out.tiers = highlight;
|
|
|
|
// 같은 상자 거듭 누름 = 한 번만 고름 · 한 번만 넘김
|
|
reset();
|
|
press(boxOf("inlet"));
|
|
press(boxOf("inlet"));
|
|
out.twice = log.filter((e) => e[0] === "select" || e[0] === "reveal").length;
|
|
|
|
process.stdout.write(JSON.stringify(out));
|