/* ============================================================================= * ui_template_float_panel.ts * 떠 있는 판 `float-panel`(552-4 틀) — 지도 · 그림 위에 뜨는 작은 판: 머리(제목 · 닫기 ✕) + 몸 · 뒷막 없음. * 꼴 = 판(머리 + 몸) · 끌기 머리(draggable · 부모 안에서 옮김) · 팝오버(popover · 머리 없음 · 몸만) * 크기 = 머리 data-size 32 · 닫기 = 아이콘 단추 24 · 폭 = width(기본 내용 폭) * 상태 = 열림 · 닫힘(hidden) · 끄는 중(`is-dragging`) * 쓰는 법 = const fp = createFloatPanel({ title: "측정", onClose: () => fp.root.remove() }); fp.body.append(table); map.append(fp.root) * 대신하는 옛 변종 = B05 `b05-routeedit__hint/__info/__measure` · `b05-crossmodal` · `masshaul-overlay` · B06 `b06-structure-panel` · * B08 `b08-milestone__tools` · B09 `.b09-popup` · CAD `.cad-quick-properties` · E03 `e03-view3d` · M02 `m02-pop` · D03 `d03-plan__cross` … * ========================================================================== */ import { createIconButton } from "./ui_template_button"; import "./ui_template_float_panel.css"; export interface FloatPanelOptions { title?: string; /** 있으면 머리에 닫기 ✕ */ onClose?: () => void; /** 머리를 잡아 부모 안에서 옮김 */ draggable?: boolean; /** 머리 없는 팝오버 */ popover?: boolean; width?: number; } export interface FloatPanel { root: HTMLElement; body: HTMLElement; } /** 머리 끌기 — 부모(위치 잡힌 칸) 안에서 left · top 만 바꿈 */ function dragBy(root: HTMLElement, head: HTMLElement): void { head.addEventListener("pointerdown", (down) => { if ((down.target as HTMLElement).closest("button")) return; const x0 = down.clientX - root.offsetLeft; const y0 = down.clientY - root.offsetTop; head.setPointerCapture(down.pointerId); root.classList.add("is-dragging"); const move = (ev: PointerEvent): void => { root.style.left = `${Math.max(0, ev.clientX - x0)}px`; root.style.top = `${Math.max(0, ev.clientY - y0)}px`; }; const up = (): void => { root.classList.remove("is-dragging"); head.removeEventListener("pointermove", move); }; head.addEventListener("pointermove", move); head.addEventListener("pointerup", up, { once: true }); }); } export function createFloatPanel(opts: FloatPanelOptions): FloatPanel { const root = document.createElement("section"); root.className = `ui-float-panel${opts.popover ? " ui-float-panel--popover" : ""}`; if (opts.width) root.style.width = `${opts.width}px`; if (!opts.popover) { const head = document.createElement("header"); head.className = "ui-float-panel__head"; head.dataset.size = "32"; const title = document.createElement("strong"); title.className = "ui-float-panel__title"; title.textContent = opts.title ?? ""; head.append(title); if (opts.onClose) head.append( createIconButton({ icon: "x", title: "닫기", size: 24, onClick: () => opts.onClose?.() }), ); if (opts.draggable) { head.classList.add("is-draggable"); dragBy(root, head); } root.append(head); if (opts.title) root.setAttribute("aria-label", opts.title); } const body = document.createElement("div"); body.className = "ui-float-panel__body"; root.append(body); return { root, body }; }