feat(종단): 325-3 유토곡선 · 배수유역 [창 분할] — 몸통 attachPopout 한 벌 · 유토 판 좌상단 · 배수 머리줄 맨 앞 단추 · 창 이름 따로 · 단추가 놓인 창에서 엶 · 뗀 판 창 꽉 채움
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -55,6 +55,35 @@
|
||||
font-size: var(--btn-bar-font-size);
|
||||
}
|
||||
|
||||
/* 유토곡선 · 배수유역 판 [창 분할](계획서 325-3) — 종단 [창 분할]과 같은 양식 · 유토는 판 좌상단에 띄움. */
|
||||
.b05-subpanel__popout {
|
||||
flex: none;
|
||||
padding: var(--btn-bar-padding);
|
||||
font-size: var(--btn-bar-font-size);
|
||||
}
|
||||
|
||||
.b05-profile__masshaul-overlay > .b05-subpanel__popout {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: var(--spacing-8);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* 새 창으로 뗀 유토곡선 · 배수유역 판 — 창 전체를 채움 · 높이 · 폭 끌기와 접기 손잡이는 감춤. */
|
||||
.b05-profile__masshaul-overlay.is-popout,
|
||||
.b05-drainage.is-popout {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.b05-profile__masshaul-overlay.is-popout > .ui-resizer,
|
||||
.b05-drainage.is-popout > .ui-resizer,
|
||||
.b05-drainage.is-popout > .ui-workflow-overlay__handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 횡단 조정(카드 조정창 사본 · 폼 칸에 옮겨 붙은 행)은 횡단 보기 몫 — 종단 보기에서는 한 벌 폼
|
||||
안에서 가린다(패널 뿌리 클래스가 보기를 따라 바뀐다). */
|
||||
.b05-route__panel .b06-adjust-dock,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_profile_popout.ts
|
||||
* 종단 하단 패널(종단도 · 표 · 유토곡선 · 배수유역도)을 **브라우저 새 창**으로 뺀다(계획서 54-35).
|
||||
* 유토곡선 · 배수유역 판은 따로 또 뗀다(계획서 325-3) — 같은 몸통(`attachPopout`), 창 이름만 다름.
|
||||
*
|
||||
* 화면 조각(DOM)만 옮긴다 — 코드 · 자료 · 가게는 메인 창 그대로, 같은 함수가 그린다.
|
||||
* 창 전역에 걸린 듣기(방향키 · 포인터 놓기 · 메뉴 닫기)는 메인 창에만 있으므로 새 창에서 난
|
||||
@@ -13,28 +14,39 @@ import type { DesignProfile } from "./design_profile";
|
||||
/** 메인 창 문서 · 창에 걸린 듣기가 받아야 할 새 창 이벤트. */
|
||||
const FORWARD_TYPES = ["keydown", "keyup", "pointerdown", "pointerup", "pointercancel"];
|
||||
|
||||
/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */
|
||||
export function attachProfilePopout(
|
||||
profile: DesignProfile,
|
||||
slot: HTMLElement,
|
||||
button: HTMLButtonElement,
|
||||
): void {
|
||||
const { root } = profile;
|
||||
/** 요소가 놓인 창(문서에 안 붙었으면 메인 창). */
|
||||
const viewOfNode = (node: Node): Window => node.ownerDocument?.defaultView ?? window;
|
||||
|
||||
/** 조각 하나를 새 창으로 떼는 단추 — 창 이름은 조각마다 달라야 함(같으면 앞 창 body 를 덮어 조각이 사라짐). */
|
||||
interface PopoutSpec {
|
||||
piece: HTMLElement;
|
||||
button: HTMLButtonElement;
|
||||
name: string;
|
||||
features: string;
|
||||
docTitle: string;
|
||||
/** 조각을 제자리로 돌림. */
|
||||
home: () => void;
|
||||
/** 뗐다 · 붙였다(조각이 옮겨 간 뒤). */
|
||||
onMove?: (popped: boolean) => void;
|
||||
}
|
||||
|
||||
/** 조각을 새 창으로 떼고 붙이는 몸통 — 새 창은 단추가 놓인 창에서 엶(종단 새 창 안에서 다시 떼기). */
|
||||
function attachPopout(profile: DesignProfile, spec: PopoutSpec): void {
|
||||
const { piece, button } = spec;
|
||||
let popup: Window | null = null;
|
||||
|
||||
button.type = "button";
|
||||
button.className = "ui-btn ui-btn--ghost b05-route-profile__popout";
|
||||
button.textContent = "창 분할";
|
||||
button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합";
|
||||
button.addEventListener("click", () => (popup && !popup.closed ? closePopup() : open()));
|
||||
|
||||
const restore = (): void => {
|
||||
if (!root.classList.contains("is-popout")) return;
|
||||
root.classList.remove("is-popout");
|
||||
if (!piece.classList.contains("is-popout")) return;
|
||||
piece.classList.remove("is-popout");
|
||||
button.textContent = "창 분할";
|
||||
slot.append(root);
|
||||
spec.home();
|
||||
popup = null;
|
||||
requestAnimationFrame(profile.redraw);
|
||||
spec.onMove?.(false);
|
||||
viewOfNode(piece).requestAnimationFrame(profile.redraw);
|
||||
};
|
||||
const closePopup = (): void => {
|
||||
popup?.close();
|
||||
@@ -49,28 +61,31 @@ export function attachProfilePopout(
|
||||
};
|
||||
|
||||
function open(): void {
|
||||
const next = window.open("", "aislo-profile-popout", "popup,width=1400,height=720");
|
||||
const opener = viewOfNode(button);
|
||||
const next = opener.open("", spec.name, spec.features);
|
||||
if (!next) {
|
||||
showToast("새 창이 막힘 — 주소창의 팝업 허용을 켠 뒤 다시 누름", "error");
|
||||
return;
|
||||
}
|
||||
popup = next;
|
||||
const source = opener.document;
|
||||
const doc = next.document;
|
||||
doc.title = "종단 — Aislo";
|
||||
// 스타일 · 테마 — 메인 창 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다.
|
||||
doc.title = spec.docTitle;
|
||||
// 스타일 · 테마 — 단추가 놓인 창의 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다.
|
||||
doc.head.replaceChildren(
|
||||
...Array.from(document.querySelectorAll('link[rel="stylesheet"], style'), (node) =>
|
||||
...Array.from(source.querySelectorAll('link[rel="stylesheet"], style'), (node) =>
|
||||
doc.importNode(node, true),
|
||||
),
|
||||
);
|
||||
Array.from(document.documentElement.attributes).forEach((attr) =>
|
||||
Array.from(source.documentElement.attributes).forEach((attr) =>
|
||||
doc.documentElement.setAttribute(attr.name, attr.value),
|
||||
);
|
||||
doc.body.className = document.body.className;
|
||||
doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${getComputedStyle(document.body).backgroundColor}`;
|
||||
root.classList.add("is-popout");
|
||||
doc.body.className = source.body.className;
|
||||
doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${opener.getComputedStyle(source.body).backgroundColor}`;
|
||||
piece.classList.add("is-popout");
|
||||
button.textContent = "창 결합";
|
||||
doc.body.replaceChildren(root);
|
||||
doc.body.replaceChildren(piece);
|
||||
spec.onMove?.(true);
|
||||
|
||||
// 새 창 키 · 포인터를 메인 문서로 — 대상은 새 창 요소 그대로(입력칸 판정 · 메뉴 안 판정).
|
||||
FORWARD_TYPES.forEach((type) =>
|
||||
@@ -93,7 +108,75 @@ export function attachProfilePopout(
|
||||
new (next as Window & typeof globalThis).ResizeObserver(() => {
|
||||
next.cancelAnimationFrame(pending);
|
||||
pending = next.requestAnimationFrame(profile.redraw);
|
||||
}).observe(root);
|
||||
}).observe(piece);
|
||||
next.addEventListener("pagehide", restore);
|
||||
}
|
||||
}
|
||||
|
||||
/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */
|
||||
export function attachProfilePopout(
|
||||
profile: DesignProfile,
|
||||
slot: HTMLElement,
|
||||
button: HTMLButtonElement,
|
||||
): void {
|
||||
button.className = "ui-btn ui-btn--ghost b05-route-profile__popout";
|
||||
button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합";
|
||||
attachPopout(profile, {
|
||||
piece: profile.root,
|
||||
button,
|
||||
name: "aislo-profile-popout",
|
||||
features: "popup,width=1400,height=720",
|
||||
docTitle: "종단 — Aislo",
|
||||
home: () => slot.append(profile.root),
|
||||
});
|
||||
}
|
||||
|
||||
/** 유토곡선 · 배수유역 판에 [창 분할] 단추를 단다 — 유토 판 좌상단 · 배수 머리줄 맨 앞(계획서 325-3). */
|
||||
export function attachSubPanelPopouts(profile: DesignProfile): void {
|
||||
const subButton = (title: string): HTMLButtonElement => {
|
||||
const button = document.createElement("button");
|
||||
button.className = "ui-btn ui-btn--ghost b05-subpanel__popout";
|
||||
button.title = title;
|
||||
return button;
|
||||
};
|
||||
|
||||
const massOverlay = profile.root.querySelector<HTMLElement>(".b05-profile__masshaul-overlay");
|
||||
const massHandle = profile.root.querySelector<HTMLElement>(".b05-profile__masshaul-handle");
|
||||
if (massOverlay && massHandle) {
|
||||
const button = subButton("유토곡선 창 분할");
|
||||
massOverlay.append(button);
|
||||
attachPopout(profile, {
|
||||
piece: massOverlay,
|
||||
button,
|
||||
name: "aislo-masshaul-popout",
|
||||
features: "popup,width=1400,height=420",
|
||||
docTitle: "유토곡선 — Aislo",
|
||||
// 제자리 = 유토 손잡이 바로 앞(종단 본문 바닥 겹침 판).
|
||||
home: () => massHandle.before(massOverlay),
|
||||
onMove: (popped) => {
|
||||
// 뗀 동안 메인 손잡이는 숨김 · 같이 줄이기 인라인 높이는 비움(새 창은 창 높이를 다 씀).
|
||||
massHandle.hidden = popped;
|
||||
massOverlay.style.height = "";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const drainage = profile.side;
|
||||
const header = drainage?.root.querySelector<HTMLElement>(".b05-drainage__header");
|
||||
if (drainage && header) {
|
||||
const button = subButton("배수유역 창 분할");
|
||||
header.prepend(button);
|
||||
const content = drainage.root.parentElement;
|
||||
attachPopout(profile, {
|
||||
piece: drainage.root,
|
||||
button,
|
||||
name: "aislo-drainage-popout",
|
||||
features: "popup,width=1000,height=720",
|
||||
docTitle: "배수유역 — Aislo",
|
||||
// 제자리 = 종단 content 끝(종단 본문 오른쪽).
|
||||
home: () => content?.append(drainage.root),
|
||||
// 지도 크기 관찰을 놓인 창 것으로(325-1).
|
||||
onMove: () => drainage.rehome(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_U
|
||||
import { drainageSide } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool";
|
||||
import { createDesignPanel, type DesignPanel } from "./design_panel";
|
||||
import { createDesignProfile, type DesignProfile } from "./design_profile";
|
||||
import { attachProfilePopout } from "./design_profile_popout";
|
||||
import { attachProfilePopout, attachSubPanelPopouts } from "./design_profile_popout";
|
||||
import { saveDesign, type DesignSaveStep } from "./design_save";
|
||||
import { resetDesign } from "./design_reset";
|
||||
import type { DesignView } from "./design_dock";
|
||||
@@ -179,6 +179,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile {
|
||||
});
|
||||
shared.profileSlot.append(profile.root);
|
||||
attachProfilePopout(profile, shared.profileSlot, popoutButton);
|
||||
attachSubPanelPopouts(profile);
|
||||
shared.profile = profile;
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/* 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() {} }),
|
||||
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));
|
||||
@@ -84,6 +84,9 @@ const window = {
|
||||
addEventListener: (t, fn) => (window.listeners[t] ??= []).push(fn),
|
||||
removeEventListener: () => {},
|
||||
open: () => popup,
|
||||
document,
|
||||
getComputedStyle: () => ({ backgroundColor: "" }),
|
||||
requestAnimationFrame: (fn) => frames.push(fn),
|
||||
};
|
||||
const popupListeners = {};
|
||||
const popup = {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""224-1c(PLAN 325-3) 유토곡선 · 배수유역 [창 분할] — 종단 [창 분할]과 같은 양식 · 따로 새 창.
|
||||
|
||||
- 단추 = 유토 판 좌상단 · 배수 머리줄 맨 앞 · 글 [창 분할]/[창 결합] · 툴팁에 판 이름.
|
||||
- 창 이름 셋 다름(같으면 앞 창 body 를 덮어 조각이 사라짐).
|
||||
- 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 지도 관찰 rehome.
|
||||
- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 유토 손잡이 바로 앞 · 배수 content 끝.
|
||||
- 종단 새 창 안에서 다시 떼면 그 창이 새 창을 엶(메인 창 팝업 차단 피함).
|
||||
|
||||
`helper_224_1c_subpanel_popout.cjs` 가 진짜 TS 를 가짜 DOM · 가짜 창 위에서 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = Path(HERE).parents[1]
|
||||
NAMES = ["aislo-profile-popout", "aislo-masshaul-popout", "aislo-drainage-popout"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_224_1c_subpanel_popout.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_단추_글_툴팁_클래스(r):
|
||||
cls = "ui-btn ui-btn--ghost b05-subpanel__popout"
|
||||
assert r["mass"] == {
|
||||
"text": "창 분할",
|
||||
"title": "유토곡선 창 분할",
|
||||
"className": cls,
|
||||
"type": "button",
|
||||
}
|
||||
assert r["drainage"] == {
|
||||
"text": "창 분할",
|
||||
"title": "배수유역 창 분할",
|
||||
"className": cls,
|
||||
"type": "button",
|
||||
}
|
||||
assert "b05-route-profile__popout" in r["profile"]["className"]
|
||||
|
||||
|
||||
def test_유토_뗌(r):
|
||||
assert r["massOpened"] == {
|
||||
"inPopupBody": True,
|
||||
"isPopout": True,
|
||||
"handleHidden": True,
|
||||
"inline": "",
|
||||
"text": "창 결합",
|
||||
"docTitle": "유토곡선 — Aislo",
|
||||
}
|
||||
|
||||
|
||||
def test_배수_뗌은_rehome(r):
|
||||
assert r["drainOpened"] == {"inPopupBody": True, "rehomes": 1, "text": "창 결합"}
|
||||
|
||||
|
||||
def test_새_창_닫힘과_단추로_제자리(r):
|
||||
assert r["massClosed"] == {
|
||||
"order": ["b05-profile__masshaul-overlay", "b05-profile__masshaul-handle"],
|
||||
"handleHidden": False,
|
||||
"isPopout": False,
|
||||
"text": "창 분할",
|
||||
}
|
||||
assert r["drainClosed"] == {"popupClosed": True, "last": True, "rehomes": 2, "text": "창 분할"}
|
||||
|
||||
|
||||
def test_창_이름_셋_다름과_여는_창(r):
|
||||
names = [name for _, name in r["opened"]]
|
||||
assert set(names) == set(NAMES) and len(set(NAMES)) == 3
|
||||
# 종단 새 창 안의 단추는 종단 새 창이 엶
|
||||
assert r["opened"][3] == ["aislo-profile-popout", "aislo-masshaul-popout"]
|
||||
assert r["nested"]["profileInPopup"] is True
|
||||
|
||||
|
||||
def test_페이지_떠나면_셋_다_닫고_제자리(r):
|
||||
assert r["disposed"] == {
|
||||
"closed": [True, True, True],
|
||||
"profileInSlot": True,
|
||||
"massBeforeHandle": True,
|
||||
"drainInContent": True,
|
||||
"handleHidden": False,
|
||||
"original": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_배선과_양식():
|
||||
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
||||
assert "attachSubPanelPopouts(profile);" in shared
|
||||
css = (ROOT / "A00_Common/design/design_page.css").read_text(encoding="utf-8")
|
||||
block = re.search(r"\n\.b05-subpanel__popout \{([^}]*)\}", css).group(1)
|
||||
assert "var(--btn-bar-padding)" in block and "var(--btn-bar-font-size)" in block
|
||||
assert ".b05-profile__masshaul-overlay.is-popout," in css
|
||||
assert ".b05-drainage.is-popout {" in css
|
||||
Reference in New Issue
Block a user