Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
215 lines
7.7 KiB
JavaScript
215 lines
7.7 KiB
JavaScript
/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout`
|
|
* (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄.
|
|
* · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름
|
|
* · 단추 = 두 판 머리줄 맨 앞(유토는 접어도 남는 머리줄 · 354-1)
|
|
* · 뗌 = 새 창 body · 배수 rehome · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(종단 열 끝 · content 끝)
|
|
* · 종단 새 창 안의 단추 = 그 창에서 엶 */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8");
|
|
|
|
// ---- 가짜 DOM — 부모 사슬 끝이 어느 문서 body 냐로 ownerDocument 를 정함 ----
|
|
let mainDoc = null;
|
|
function el(tag, className = "") {
|
|
const node = {
|
|
tagName: tag.toUpperCase(),
|
|
className,
|
|
textContent: "",
|
|
title: "",
|
|
type: "",
|
|
hidden: false,
|
|
children: [],
|
|
parent: null,
|
|
listeners: {},
|
|
style: { height: "", cssText: "" },
|
|
};
|
|
node.classList = {
|
|
list: () => node.className.split(/\s+/).filter(Boolean),
|
|
contains: (c) => node.classList.list().includes(c),
|
|
add: (c) =>
|
|
!node.classList.contains(c) && (node.className = [...node.classList.list(), c].join(" ")),
|
|
remove: (c) =>
|
|
(node.className = node.classList
|
|
.list()
|
|
.filter((x) => x !== c)
|
|
.join(" ")),
|
|
};
|
|
Object.defineProperty(node, "parentElement", { get: () => node.parent });
|
|
Object.defineProperty(node, "ownerDocument", {
|
|
get: () => {
|
|
let top = node;
|
|
while (top.parent) top = top.parent;
|
|
return top.doc ?? mainDoc;
|
|
},
|
|
});
|
|
node.detach = () => {
|
|
if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node);
|
|
node.parent = null;
|
|
};
|
|
node.append = (...kids) =>
|
|
kids.forEach((k) => (k.detach(), (k.parent = node), node.children.push(k)));
|
|
node.prepend = (...kids) =>
|
|
kids.reverse().forEach((k) => (k.detach(), (k.parent = node), node.children.unshift(k)));
|
|
node.replaceChildren = (...kids) => {
|
|
node.children.slice().forEach((c) => c.detach());
|
|
node.append(...kids);
|
|
};
|
|
node.before = (other) => {
|
|
const parent = node.parent;
|
|
other.detach();
|
|
other.parent = parent;
|
|
parent.children.splice(parent.children.indexOf(node), 0, other);
|
|
};
|
|
node.querySelector = (sel) => {
|
|
const cls = sel.replace(/^\./, "");
|
|
for (const kid of node.children) {
|
|
if (kid.classList.contains(cls)) return kid;
|
|
const deep = kid.querySelector(sel);
|
|
if (deep) return deep;
|
|
}
|
|
return null;
|
|
};
|
|
node.addEventListener = (type, fn) => (node.listeners[type] ??= []).push(fn);
|
|
node.click = () => (node.listeners.click ?? []).forEach((fn) => fn({}));
|
|
return node;
|
|
}
|
|
function fakeDoc(view) {
|
|
const doc = { title: "", defaultView: view };
|
|
doc.head = el("head");
|
|
doc.body = el("body");
|
|
doc.body.doc = doc;
|
|
doc.documentElement = Object.assign(el("html"), { attributes: [], setAttribute() {} });
|
|
doc.querySelectorAll = () => [];
|
|
doc.importNode = (n) => n;
|
|
doc.createElement = (tag) => el(tag);
|
|
return doc;
|
|
}
|
|
const opened = []; // [여는 창 이름, 새 창 이름]
|
|
function fakeWindow(label) {
|
|
const win = { label, listeners: {}, closed: false, frames: 0 };
|
|
win.document = fakeDoc(win);
|
|
win.addEventListener = (t, fn) => (win.listeners[t] ??= []).push(fn);
|
|
win.removeEventListener = () => {};
|
|
win.getComputedStyle = () => ({ backgroundColor: "" });
|
|
win.requestAnimationFrame = () => (win.frames += 1);
|
|
win.cancelAnimationFrame = () => {};
|
|
win.ResizeObserver = class {
|
|
observe() {}
|
|
};
|
|
win.close = () => (win.closed = true);
|
|
win.fire = (t) => (win.listeners[t] ?? []).forEach((fn) => fn({}));
|
|
win.open = (_url, name) => {
|
|
const next = fakeWindow(name);
|
|
opened.push([label, name]);
|
|
return next;
|
|
};
|
|
return win;
|
|
}
|
|
const main = fakeWindow("main");
|
|
mainDoc = main.document;
|
|
|
|
const js = ts.transpileModule(read("A00_Common", "design", "design_profile_popout.ts"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const mod = {};
|
|
new Function("exports", "require", "window", "document", js)(
|
|
mod,
|
|
() => ({ showToast() {} }),
|
|
main,
|
|
mainDoc,
|
|
);
|
|
|
|
// ---- 종단 뿌리 = [content = [열 = [종단 칸, 유토 판 = [머리줄 = [손잡이]]], 배수 판 = [머리줄 = [제목]]]] ----
|
|
const slot = el("div");
|
|
mainDoc.body.append(slot);
|
|
const root = el("div", "b05-route-profile");
|
|
slot.append(root);
|
|
const content = el("div", "b05-route-profile__content");
|
|
const column = el("div", "b05-route-profile__column");
|
|
const bodyWrap = el("div", "b05-route-profile__body-wrap");
|
|
const massOverlay = el("div", "b05-profile__masshaul-overlay");
|
|
const massHead = el("div", "b05-profile__masshaul-head");
|
|
const massHandle = el("div", "b05-profile__masshaul-handle");
|
|
massHead.append(massHandle);
|
|
massOverlay.append(el("div", "ui-resizer"), massHead);
|
|
column.append(bodyWrap, massOverlay);
|
|
const drainageRoot = el("aside", "b05-drainage");
|
|
const header = el("div", "b05-drainage__header");
|
|
header.append(el("h3"));
|
|
drainageRoot.append(header);
|
|
content.append(column, drainageRoot);
|
|
root.append(content);
|
|
let rehomes = 0;
|
|
let disposed = 0;
|
|
const profile = {
|
|
root,
|
|
redraw() {},
|
|
dispose: () => (disposed += 1),
|
|
side: { root: drainageRoot, rehome: () => (rehomes += 1) },
|
|
};
|
|
const profileButton = el("button");
|
|
mod.attachProfilePopout(profile, slot, profileButton);
|
|
mod.attachSubPanelPopouts(profile);
|
|
|
|
const massButton = massHead.children[0];
|
|
const drainButton = header.children[0];
|
|
const btn = (b) => ({ text: b.textContent, title: b.title, className: b.className, type: b.type });
|
|
const out = { mass: btn(massButton), drainage: btn(drainButton), profile: btn(profileButton) };
|
|
|
|
out.massButtonInHead = massButton.tagName === "BUTTON" && massHead.children[1] === massHandle;
|
|
// 유토 뗌
|
|
massButton.click();
|
|
const massPopup = massOverlay.parent?.doc?.defaultView;
|
|
out.massOpened = {
|
|
inPopupBody: massOverlay.parent === massPopup?.document.body,
|
|
isPopout: massOverlay.classList.contains("is-popout"),
|
|
text: massButton.textContent,
|
|
docTitle: massPopup?.document.title,
|
|
};
|
|
// 배수 뗌
|
|
drainButton.click();
|
|
const drainPopup = drainageRoot.parent?.doc?.defaultView;
|
|
out.drainOpened = {
|
|
inPopupBody: drainageRoot.parent === drainPopup?.document.body,
|
|
rehomes,
|
|
text: drainButton.textContent,
|
|
};
|
|
// 유토 새 창 닫힘 → 제자리(종단 열 끝)
|
|
massPopup.fire("pagehide");
|
|
out.massClosed = {
|
|
order: column.children.map((c) => c.className),
|
|
isPopout: massOverlay.classList.contains("is-popout"),
|
|
text: massButton.textContent,
|
|
};
|
|
// 배수 단추 다시 누름 → 창 닫고 content 끝
|
|
drainButton.click();
|
|
out.drainClosed = {
|
|
popupClosed: drainPopup.closed,
|
|
last: content.children[content.children.length - 1] === drainageRoot,
|
|
rehomes,
|
|
text: drainButton.textContent,
|
|
};
|
|
|
|
// 종단 새 창 → 그 안에서 유토 뗌 = 종단 새 창이 엶
|
|
profileButton.click();
|
|
const profilePopup = root.parent?.doc?.defaultView;
|
|
massButton.click();
|
|
const massPopup2 = massOverlay.parent?.doc?.defaultView;
|
|
out.nested = { profileInPopup: !!profilePopup && profilePopup !== main };
|
|
// 페이지 떠남(dispose) → 셋 다 닫히고 제자리
|
|
drainButton.click();
|
|
const drainPopup2 = drainageRoot.parent?.doc?.defaultView;
|
|
profile.dispose();
|
|
out.disposed = {
|
|
closed: [profilePopup.closed, massPopup2.closed, drainPopup2.closed],
|
|
profileInSlot: root.parent === slot,
|
|
massInColumnEnd: column.children[column.children.length - 1] === massOverlay,
|
|
drainInContent: drainageRoot.parent === content,
|
|
original: disposed,
|
|
};
|
|
out.opened = opened;
|
|
process.stdout.write(JSON.stringify(out));
|