- 종단 도구줄 [배수유역도] 뒤 [새 창] 단추 — 종단도·표·유토곡선·배수유역도 묶음을 같은 출처 팝업으로 옮김 · 코드·자료·가게는 메인 창 그대로 - 스타일 시트·html 테마 속성 복사 · 팝업 크기 변화 시 팝업 ResizeObserver 로 다시 그림 - 팝업 키·포인터(keydown·keyup·pointerdown·pointerup·pointercancel)·blur 를 메인 문서로 넘김 — 대상 요소 유지 - 팝업 닫힘·[제자리로]·메인 창 떠남·페이지 정리 때 원래 자리로 되돌림 · 팝업 차단 시 알림 한 줄 - 우클릭 메뉴 화면 밖 보정을 요소 소속 창 기준으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
/* =============================================================================
|
|
* 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"];
|
|
|
|
/** 종단 도구줄 [배수유역도] 단추 뒤에 [새 창] 단추를 붙인다 — 두 보기가 같은 한 벌을 쓴다. */
|
|
export function attachProfilePopout(
|
|
profile: DesignProfile,
|
|
slot: HTMLElement,
|
|
sideTool: HTMLElement | null,
|
|
): void {
|
|
const { root } = profile;
|
|
let popup: Window | null = null;
|
|
|
|
const button = document.createElement("button");
|
|
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()));
|
|
sideTool?.after(button);
|
|
|
|
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<string, typeof Event>)[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);
|
|
}
|
|
}
|