/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout` * (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄. * · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름 * · 뗌 = 새 창 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() {}, releaseSelectAfterPick() {} }), main, mainDoc, ); // ---- 종단 뿌리 = [content = [bodyWrap = [유토 판, 유토 손잡이], 배수 판 = [머리줄 = [제목]]]] ---- 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 bodyWrap = el("div"); const massOverlay = el("div", "b05-profile__masshaul-overlay"); const massHandle = el("div", "b05-profile__masshaul-handle"); bodyWrap.append(massOverlay, massHandle); const drainageRoot = el("aside", "b05-drainage"); const header = el("div", "b05-drainage__header"); header.append(el("h3")); drainageRoot.append(header); content.append(bodyWrap, 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 = massOverlay.children.find((c) => c.tagName === "BUTTON"); 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) }; // 유토 뗌 massOverlay.style.height = "123px"; massButton.click(); const massPopup = massOverlay.parent?.doc?.defaultView; out.massOpened = { inPopupBody: massOverlay.parent === massPopup?.document.body, isPopout: massOverlay.classList.contains("is-popout"), handleHidden: massHandle.hidden, inline: massOverlay.style.height, 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: bodyWrap.children.map((c) => c.className), handleHidden: massHandle.hidden, 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, massBeforeHandle: bodyWrap.children.indexOf(massOverlay) === bodyWrap.children.indexOf(massHandle) - 1, drainInContent: drainageRoot.parent === content, handleHidden: massHandle.hidden, original: disposed, }; out.opened = opened; process.stdout.write(JSON.stringify(out));