/* ============================================================================= * A00_Common/design/design_profile_popout.ts * 종단 하단 패널(종단도 · 표 · 유토곡선 · 배수유역도)을 **브라우저 새 창**으로 뺀다(계획서 54-35). * * 화면 조각(DOM)만 옮긴다 — 코드 · 자료 · 가게는 메인 창 그대로, 같은 함수가 그린다. * 창 전역에 걸린 듣기(방향키 · 포인터 놓기 · 메뉴 닫기)는 메인 창에만 있으므로 새 창에서 난 * 키 · 포인터를 메인 문서로 넘긴다. 새 창을 닫거나 메인 창이 떠나면 조각을 제자리로 돌린다. * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; 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; 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"); button.textContent = "창 분할"; slot.append(root); popup = null; requestAnimationFrame(profile.redraw); }; const closePopup = (): void => { popup?.close(); restore(); }; window.addEventListener("pagehide", closePopup); const dispose = profile.dispose; profile.dispose = () => { window.removeEventListener("pagehide", closePopup); closePopup(); dispose(); }; function open(): void { const next = window.open("", "aislo-profile-popout", "popup,width=1400,height=720"); if (!next) { showToast("새 창이 막힘 — 주소창의 팝업 허용을 켠 뒤 다시 누름", "error"); return; } popup = next; const doc = next.document; doc.title = "종단 — Aislo"; // 스타일 · 테마 — 메인 창 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다. doc.head.replaceChildren( ...Array.from(document.querySelectorAll('link[rel="stylesheet"], style'), (node) => doc.importNode(node, true), ), ); Array.from(document.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"); button.textContent = "창 결합"; doc.body.replaceChildren(root); // 새 창 키 · 포인터를 메인 문서로 — 대상은 새 창 요소 그대로(입력칸 판정 · 메뉴 안 판정). FORWARD_TYPES.forEach((type) => next.addEventListener( type, (event) => { const Ctor = (window as unknown as Record)[event.constructor.name]; const copy = new (Ctor ?? Event)(type, event); Object.defineProperty(copy, "target", { value: event.target }); document.dispatchEvent(copy); if (copy.defaultPrevented) event.preventDefault(); }, true, ), ); // 새 창이 초점을 잃으면 길게 누르기를 끊는다(메인 창 blur 듣기). next.addEventListener("blur", () => window.dispatchEvent(new FocusEvent("blur"))); // 새 창 크기 기준으로 다시 맞춘다. let pending = 0; new (next as Window & typeof globalThis).ResizeObserver(() => { next.cancelAnimationFrame(pending); pending = next.requestAnimationFrame(profile.redraw); }).observe(root); next.addEventListener("pagehide", restore); } }